From c664d19e7d4a0aa0762c30a72ae238cf818891ab Mon Sep 17 00:00:00 2001 From: Matthias Baumgartner Date: Wed, 11 Jan 2023 21:20:47 +0100 Subject: Feature support in the schema * Type annotations * Feature type * Moved from_string from Schema to its own file/function * Root predicate has a valid (not-None) range * ROOT_... export in schema.types * Empty as the default Schema constructor * Schema loads some additional default symbols * _Type instances compare along class hierarchy --- bsfs/schema/__init__.py | 5 +- bsfs/schema/schema.py | 111 +---- bsfs/schema/serialize.py | 143 ++++++ bsfs/schema/types.py | 183 +++++++- test/schema/test_schema.py | 271 ++--------- test/schema/test_serialize.py | 1007 +++++++++++++++++++++++++++++++++++++++++ test/schema/test_types.py | 240 ++++++++-- 7 files changed, 1578 insertions(+), 382 deletions(-) create mode 100644 bsfs/schema/serialize.py create mode 100644 test/schema/test_serialize.py diff --git a/bsfs/schema/__init__.py b/bsfs/schema/__init__.py index ad4d456..dc24313 100644 --- a/bsfs/schema/__init__.py +++ b/bsfs/schema/__init__.py @@ -9,7 +9,8 @@ import typing # inner-module imports from .schema import Schema -from .types import Literal, Node, Predicate +from .serialize import from_string, to_string +from .types import Literal, Node, Predicate, _Vertex # FIXME: _Vertex # exports __all__: typing.Sequence[str] = ( @@ -17,6 +18,8 @@ __all__: typing.Sequence[str] = ( 'Node', 'Predicate', 'Schema', + 'from_string', + 'to_string', ) ## EOF ## diff --git a/bsfs/schema/schema.py b/bsfs/schema/schema.py index c5d4571..1c4c807 100644 --- a/bsfs/schema/schema.py +++ b/bsfs/schema/schema.py @@ -51,11 +51,13 @@ class Schema(): def __init__( self, - predicates: typing.Iterable[types.Predicate], + predicates: typing.Optional[typing.Iterable[types.Predicate]] = None, nodes: typing.Optional[typing.Iterable[types.Node]] = None, literals: typing.Optional[typing.Iterable[types.Literal]] = None, ): # materialize arguments + if predicates is None: + predicates = set() if nodes is None: nodes = set() if literals is None: @@ -63,24 +65,36 @@ class Schema(): nodes = set(nodes) literals = set(literals) predicates = set(predicates) + + # add root types to the schema + nodes.add(types.ROOT_NODE) + literals.add(types.ROOT_LITERAL) + predicates.add(types.ROOT_PREDICATE) + # add minimally necessary types to the schema + literals.add(types.ROOT_NUMBER) + predicates.add(types.ROOT_FEATURE) + # include parents in predicates set # TODO: review type annotations and ignores for python >= 3.11 (parents is _Type but should be typing.Self) predicates |= {par for pred in predicates for par in pred.parents()} # type: ignore [misc] # include predicate domain in nodes set nodes |= {pred.domain for pred in predicates} # include predicate range in nodes and literals sets - prange = {pred.range for pred in predicates if pred.range is not None} + prange = {pred.range for pred in predicates} nodes |= {vert for vert in prange if isinstance(vert, types.Node)} literals |= {vert for vert in prange if isinstance(vert, types.Literal)} + # NOTE: ROOT_PREDICATE has a _Vertex as range which is neither in nodes nor literals + # FIXME: with the ROOT_VERTEX missing, the schema is not complete anymore! + # include parents in nodes and literals sets - # NOTE: Must be done after predicate domain/range was handled - # so that their parents are included as well. + # NOTE: Must come after predicate domain/range was handled to have their parents as well. nodes |= {par for node in nodes for par in node.parents()} # type: ignore [misc] literals |= {par for lit in literals for par in lit.parents()} # type: ignore [misc] # assign members self._nodes = {node.uri: node for node in nodes} self._literals = {lit.uri: lit for lit in literals} self._predicates = {pred.uri: pred for pred in predicates} + # verify unique uris if len(nodes) != len(self._nodes): raise errors.ConsistencyError('inconsistent nodes') @@ -214,6 +228,7 @@ class Schema(): >>> Schema.Union([a, b, c]) """ + # FIXME: copy type annotations? if len(args) == 0: raise TypeError('Schema.Union requires at least one argument (Schema or Iterable)') if isinstance(args[0], cls): # args is sequence of Schema instances @@ -295,92 +310,4 @@ class Schema(): """Return the Literal matching the *uri*.""" return self._literals[uri] - - ## constructors ## - - - @classmethod - def Empty(cls) -> 'Schema': # pylint: disable=invalid-name # capitalized classmethod - """Return a minimal Schema.""" - node = types.Node(ns.bsfs.Node, None) - literal = types.Literal(ns.bsfs.Literal, None) - predicate = types.Predicate( - uri=ns.bsfs.Predicate, - parent=None, - domain=node, - range=None, - unique=False, - ) - return cls((predicate, ), (node, ), (literal, )) - - - @classmethod - def from_string(cls, schema: str) -> 'Schema': # pylint: disable=invalid-name # capitalized classmethod - """Load and return a Schema from a string.""" - # parse string into rdf graph - graph = rdflib.Graph() - graph.parse(data=schema, format='turtle') - - def _fetch_hierarchically(factory, curr): - # emit current node - yield curr - # walk through childs - for child in graph.subjects(rdflib.URIRef(ns.rdfs.subClassOf), rdflib.URIRef(curr.uri)): - # convert to URI - child = URI(child) - # check circular dependency - if child == curr.uri or child in {node.uri for node in curr.parents()}: - raise errors.ConsistencyError('circular dependency') - # recurse and emit (sub*)childs - yield from _fetch_hierarchically(factory, factory(child, curr)) - - # fetch nodes - nodes = set(_fetch_hierarchically(types.Node, types.Node(ns.bsfs.Node, None))) - nodes_lut = {node.uri: node for node in nodes} - if len(nodes_lut) != len(nodes): - raise errors.ConsistencyError('inconsistent nodes') - - # fetch literals - literals = set(_fetch_hierarchically(types.Literal, types.Literal(ns.bsfs.Literal, None))) - literals_lut = {lit.uri: lit for lit in literals} - if len(literals_lut) != len(literals): - raise errors.ConsistencyError('inconsistent literals') - - # fetch predicates - def build_predicate(uri, parent): - uri = rdflib.URIRef(uri) - # get domain - domains = set(graph.objects(uri, rdflib.RDFS.domain)) - if len(domains) != 1: - raise errors.ConsistencyError(f'inconsistent domain: {domains}') - dom = nodes_lut.get(next(iter(domains))) - if dom is None: - raise errors.ConsistencyError('missing domain') - # get range - ranges = set(graph.objects(uri, rdflib.RDFS.range)) - if len(ranges) != 1: - raise errors.ConsistencyError(f'inconsistent range: {ranges}') - rng = next(iter(ranges)) - rng = nodes_lut.get(rng, literals_lut.get(rng)) - if rng is None: - raise errors.ConsistencyError('missing range') - # get unique flag - uniques = set(graph.objects(uri, rdflib.URIRef(ns.bsfs.unique))) - if len(uniques) != 1: - raise errors.ConsistencyError(f'inconsistent unique flags: {uniques}') - unique = bool(next(iter(uniques))) - # build Predicate - return types.Predicate(URI(uri), parent, dom, rng, unique) - - root_predicate = types.Predicate( - uri=ns.bsfs.Predicate, - parent=None, - domain=nodes_lut[ns.bsfs.Node], - range=None, # FIXME: Unclear how to handle this! Can be either a Literal or a Node - unique=False, - ) - predicates = _fetch_hierarchically(build_predicate, root_predicate) - # return Schema - return cls(predicates, nodes, literals) - ## EOF ## diff --git a/bsfs/schema/serialize.py b/bsfs/schema/serialize.py new file mode 100644 index 0000000..1222aa6 --- /dev/null +++ b/bsfs/schema/serialize.py @@ -0,0 +1,143 @@ +""" + +Part of the BlackStar filesystem (bsfs) module. +A copy of the license is provided with the project. +Author: Matthias Baumgartner, 2022 +""" +# standard imports +from collections import abc +import itertools +import typing + +# external imports +import rdflib + +# bsfs imports +from bsfs.namespace import ns +from bsfs.utils import errors, URI, typename + +# inner-module imports +from . import types +from . import schema + +# exports +__all__: typing.Sequence[str] = ( + 'to_string', + 'from_string', + ) + + +## code ## + +def from_string(schema_str: str) -> schema.Schema: + """Load and return a Schema from a string.""" + # parse string into rdf graph + graph = rdflib.Graph() + graph.parse(data=schema_str, format='turtle') + + # helper functions + def _convert(value): + """Convert the subject type from rdflib to a bsfs native type.""" + if isinstance(value, rdflib.Literal): + return value.value + if isinstance(value, rdflib.URIRef): + return URI(value) + raise errors.BackendError(f'expected Literal or URIRef, found {typename(value)}') + + def _fetch_hierarchically(factory, curr): + """Walk through a rdfs:subClassOf hierarchy, creating symbols along the way.""" + # emit current node + yield curr + # walk through childs + for child in graph.subjects(rdflib.URIRef(ns.rdfs.subClassOf), rdflib.URIRef(curr.uri)): + # fetch annotations + annotations = { + URI(pred): _convert(value) + for pred, value # FIXME: preserve datatype of value?! + in graph.predicate_objects(child) + if URI(pred) != ns.rdfs.subClassOf + } + # convert child to URI + child = URI(child) + # check circular dependency + if child == curr.uri or child in {node.uri for node in curr.parents()}: + raise errors.ConsistencyError('circular dependency') + # recurse and emit (sub*)childs + yield from _fetch_hierarchically(factory, factory(child, curr, **annotations)) + + # fetch nodes + nodes = set(_fetch_hierarchically(types.Node, types.ROOT_NODE)) + nodes_lut = {node.uri: node for node in nodes} + if len(nodes_lut) != len(nodes): + raise errors.ConsistencyError('inconsistent nodes') + + # fetch literals + literals = set(_fetch_hierarchically(types.Literal, types.ROOT_LITERAL)) + literals_lut = {lit.uri: lit for lit in literals} + if len(literals_lut) != len(literals): + raise errors.ConsistencyError('inconsistent literals') + + # fetch predicates + # FIXME: type annotation + def _fetch_value(subject: URI, predicate: rdflib.URIRef, value_factory) -> typing.Optional[typing.Any]: + """Fetch the object of a given subject and predicate. Raises a `errors.ConsistencyError` if multiple objects match.""" + values = list(graph.objects(rdflib.URIRef(subject), predicate)) + if len(values) == 0: + return None + elif len(values) == 1: + return value_factory(values[0]) + else: + raise errors.ConsistencyError(f'{subject} has multiple values for predicate {str(predicate)}, expected zero or one') + + def _build_predicate(uri, parent, **annotations): + """Predicate factory.""" + # break out on root feature type + if uri == types.ROOT_FEATURE.uri: + return types.ROOT_FEATURE + # clean annotations + annotations.pop(ns.rdfs.domain, None) + annotations.pop(ns.rdfs.range, None) + annotations.pop(ns.bsfs.unique, None) + # get domain + dom = _fetch_value(uri, rdflib.RDFS.domain, URI) + if dom is not None and dom not in nodes_lut: + raise errors.ConsistencyError(f'predicate {uri} has undefined domain {dom}') + elif dom is not None: + dom = nodes_lut[dom] + # get range + rng = _fetch_value(uri, rdflib.RDFS.range, URI) + if rng is not None and rng not in nodes_lut and rng not in literals_lut: + raise errors.ConsistencyError(f'predicate {uri} has undefined range {rng}') + elif rng is not None: + rng = nodes_lut.get(rng, literals_lut.get(rng)) + # get unique + unique = _fetch_value(uri, rdflib.URIRef(ns.bsfs.unique), bool) + # handle feature types + if isinstance(parent, types.Feature): + # clean annotations + annotations.pop(ns.bsfs.dimension, None) + annotations.pop(ns.bsfs.dtype, None) + annotations.pop(ns.bsfs.distance, None) + # get dimension + dimension = _fetch_value(uri, rdflib.URIRef(ns.bsfs.dimension), int) + # get dtype + dtype = _fetch_value(uri, rdflib.URIRef(ns.bsfs.dtype), URI) + # get distance + distance = _fetch_value(uri, rdflib.URIRef(ns.bsfs.distance), URI) + # return feature + return parent.get_child(URI(uri), domain=dom, range=rng, unique=unique, + dtype=dtype, dimension=dimension, distance=distance, **annotations) + # handle non-feature predicate + return parent.get_child(URI(uri), domain=dom, range=rng, unique=unique, **annotations) + predicates = _fetch_hierarchically(_build_predicate, types.ROOT_PREDICATE) + + return schema.Schema(predicates, nodes, literals) + + + +def to_string(schema_inst: schema.Schema) -> str: + """ + """ + raise NotImplementedError() + +## EOF ## diff --git a/bsfs/schema/types.py b/bsfs/schema/types.py index 54a7e99..e737263 100644 --- a/bsfs/schema/types.py +++ b/bsfs/schema/types.py @@ -8,6 +8,7 @@ Author: Matthias Baumgartner, 2022 import typing # bsfs imports +from bsfs.namespace import ns from bsfs.utils import errors, URI, typename # exports @@ -15,6 +16,7 @@ __all__: typing.Sequence[str] = ( 'Literal', 'Node', 'Predicate', + 'Feature', ) @@ -99,9 +101,11 @@ class _Type(): self, uri: URI, parent: typing.Optional['_Type'] = None, + **annotations: typing.Any, ): self.uri = uri self.parent = parent + self.annotations = annotations def parents(self) -> typing.Generator['_Type', None, None]: """Generate a list of parent nodes.""" @@ -110,9 +114,17 @@ class _Type(): yield curr curr = curr.parent - def get_child(self, uri: URI, **kwargs): + def get_child( + self, + uri: URI, + **kwargs, + ): """Return a child of the current class.""" - return type(self)(uri, self, **kwargs) + return type(self)( + uri=uri, + parent=self, + **kwargs + ) def __str__(self) -> str: return f'{typename(self)}({self.uri})' @@ -138,7 +150,7 @@ class _Type(): def __lt__(self, other: typing.Any) -> bool: """Return True iff *self* is a true subclass of *other*.""" - if not type(self) is type(other): # type mismatch # pylint: disable=unidiomatic-typecheck + if not isinstance(other, type(self)): return NotImplemented if self.uri == other.uri: # equivalence return False @@ -151,7 +163,7 @@ class _Type(): def __le__(self, other: typing.Any) -> bool: """Return True iff *self* is equivalent or a subclass of *other*.""" - if not type(self) is type(other): # type mismatch # pylint: disable=unidiomatic-typecheck + if not isinstance(other, type(self)): return NotImplemented if self.uri == other.uri: # equivalence return True @@ -164,7 +176,7 @@ class _Type(): def __gt__(self, other: typing.Any) -> bool: """Return True iff *self* is a true superclass of *other*.""" - if not type(self) is type(other): # type mismatch # pylint: disable=unidiomatic-typecheck + if not isinstance(other, type(self)): return NotImplemented if self.uri == other.uri: # equivalence return False @@ -177,7 +189,7 @@ class _Type(): def __ge__(self, other: typing.Any) -> bool: """Return True iff *self* is eqiuvalent or a superclass of *other*.""" - if not type(self) is type(other): # type mismatch # pylint: disable=unidiomatic-typecheck + if not isinstance(other, type(self)): return NotImplemented if self.uri == other.uri: # equivalence return True @@ -191,30 +203,33 @@ class _Type(): class _Vertex(_Type): """Graph vertex types. Can be a Node or a Literal.""" - def __init__(self, uri: URI, parent: typing.Optional['_Vertex']): - super().__init__(uri, parent) + parent: typing.Optional['_Vertex'] + def __init__(self, uri: URI, parent: typing.Optional['_Vertex'], **kwargs): + super().__init__(uri, parent, **kwargs) class Node(_Vertex): """Node type.""" - def __init__(self, uri: URI, parent: typing.Optional['Node']): - super().__init__(uri, parent) + parent: typing.Optional['Node'] + def __init__(self, uri: URI, parent: typing.Optional['Node'], **kwargs): + super().__init__(uri, parent, **kwargs) class Literal(_Vertex): """Literal type.""" - def __init__(self, uri: URI, parent: typing.Optional['Literal']): - super().__init__(uri, parent) + parent: typing.Optional['Literal'] + def __init__(self, uri: URI, parent: typing.Optional['Literal'] ,**kwargs): + super().__init__(uri, parent, **kwargs) class Predicate(_Type): - """Predicate type.""" + """Predicate base type.""" # source type. domain: Node # destination type. - range: typing.Optional[typing.Union[Node, Literal]] + range: _Vertex # maximum cardinality of type. unique: bool @@ -223,25 +238,26 @@ class Predicate(_Type): self, # Type members uri: URI, - parent: typing.Optional['Predicate'], + parent: '_PredicateBase', # Predicate members domain: Node, - range: typing.Optional[typing.Union[Node, Literal]], # pylint: disable=redefined-builtin + range: _Vertex, # pylint: disable=redefined-builtin unique: bool, + **kwargs, ): # check arguments if not isinstance(domain, Node): raise TypeError(domain) - if range is not None and not isinstance(range, Node) and not isinstance(range, Literal): + if range != ROOT_VERTEX and not isinstance(range, (Node, Literal)): raise TypeError(range) # initialize - super().__init__(uri, parent) + super().__init__(uri, parent, **kwargs) self.domain = domain self.range = range - self.unique = unique + self.unique = bool(unique) def __hash__(self) -> int: - return hash((super().__hash__(), self.domain, self.range, self.unique)) + return hash((super().__hash__(), self.domain, self.unique, self.range)) def __eq__(self, other: typing.Any) -> bool: return super().__eq__(other) \ @@ -264,13 +280,132 @@ class Predicate(_Type): raise errors.ConsistencyError(f'{domain} must be a subclass of {self.domain}') if range is None: range = self.range - if range is None: # inherited range from ns.bsfs.Predicate - raise ValueError('range must be defined by the parent or argument') - if self.range is not None and not range <= self.range: + # NOTE: The root predicate has a Vertex as range, which is neither a parent of the root + # Node nor Literal. Hence, that test is skipped since a child should be allowed to + # specialize from Vertex to anything. + if self.range != ROOT_VERTEX and not range <= self.range: raise errors.ConsistencyError(f'{range} must be a subclass of {self.range}') if unique is None: unique = self.unique - return super().get_child(uri, domain=domain, range=range, unique=unique, **kwargs) + return super().get_child( + uri=uri, + domain=domain, + range=range, + unique=unique, + **kwargs + ) + + +class Feature(Predicate): + """Feature base type.""" + + # Number of feature vector dimensions. + dimension: int + + # Feature vector datatype. + dtype: URI + + # Distance measure to compare feature vectors. + distance: URI + + def __init__( + self, + # Type members + uri: URI, + parent: Predicate, + # Predicate members + domain: Node, + range: Literal, + unique: bool, + # Feature members + dimension: int, + dtype: URI, + distance: URI, + **kwargs, + ): + super().__init__(uri, parent, domain, range, unique, **kwargs) + self.dimension = int(dimension) + self.dtype = URI(dtype) + self.distance = URI(distance) + + def __hash__(self) -> int: + return hash((super().__hash__(), self.dimension, self.dtype, self.distance)) + + def __eq__(self, other: typing.Any) -> bool: + return super().__eq__(other) \ + and self.dimension == other.dimension \ + and self.dtype == other.dtype \ + and self.distance == other.distance + + def get_child( + self, + uri: URI, + domain: typing.Optional[Node] = None, + range: typing.Optional[Literal] = None, # pylint: disable=redefined-builtin + unique: typing.Optional[bool] = None, + dimension: typing.Optional[int] = None, + dtype: typing.Optional[URI] = None, + distance: typing.Optional[URI] = None, + **kwargs, + ): + """Return a child of the current class.""" + if dimension is None: + dimension = self.dimension + if dtype is None: + dtype = self.dtype + if distance is None: + distance = self.distance + return super().get_child( + uri=uri, + domain=domain, + range=range, + unique=unique, + dimension=dimension, + dtype=dtype, + distance=distance, + **kwargs, + ) +# essential vertices +ROOT_VERTEX = _Vertex( + uri=ns.bsfs.Vertex, + parent=None, + ) + +ROOT_NODE = Node( + uri=ns.bsfs.Node, + parent=None, + ) + +ROOT_LITERAL = Literal( + uri=ns.bsfs.Literal, + parent=None, + ) + +ROOT_NUMBER = Literal( + uri=ns.bsfs.Number, + parent=ROOT_LITERAL, + ) + +# essential predicates +ROOT_PREDICATE = Predicate( + uri=ns.bsfs.Predicate, + parent=None, + domain=ROOT_NODE, + range=ROOT_VERTEX, + unique=False, + ) + +ROOT_FEATURE = Feature( + uri=ns.bsfs.Feature, + parent=ROOT_PREDICATE, + domain=ROOT_NODE, + range=ROOT_LITERAL, + unique=False, + dimension=1, + dtype=ns.bsfs.f16, + distance=ns.bsfs.euclidean, + ) + ## EOF ## diff --git a/test/schema/test_schema.py b/test/schema/test_schema.py index 888cdca..1b45db0 100644 --- a/test/schema/test_schema.py +++ b/test/schema/test_schema.py @@ -10,7 +10,7 @@ import unittest # bsfs imports from bsfs.namespace import ns -from bsfs.schema import types +from bsfs.schema import types, from_string from bsfs.utils import errors # objects to test @@ -55,7 +55,7 @@ class TestSchema(unittest.TestCase): ''' # nodes - self.n_root = types.Node(ns.bsfs.Node, None) + self.n_root = types.ROOT_NODE self.n_ent = types.Node(ns.bsfs.Entity, types.Node(ns.bsfs.Node, None)) self.n_img = types.Node(ns.bsfs.Image, types.Node(ns.bsfs.Entity, types.Node(ns.bsfs.Node, None))) self.n_tag = types.Node(ns.bsfs.Tag, types.Node(ns.bsfs.Node, None)) @@ -63,24 +63,32 @@ class TestSchema(unittest.TestCase): self.nodes = [self.n_root, self.n_ent, self.n_img, self.n_tag, self.n_unused] # literals - self.l_root = types.Literal(ns.bsfs.Literal, None) + self.l_root = types.ROOT_LITERAL + self.l_number = types.ROOT_NUMBER self.l_string = types.Literal(ns.xsd.string, types.Literal(ns.bsfs.Literal, None)) - self.l_integer = types.Literal(ns.xsd.integer, types.Literal(ns.bsfs.Literal, None)) + self.l_integer = types.Literal(ns.xsd.integer, self.l_number) self.l_unused = types.Literal(ns.xsd.boolean, types.Literal(ns.bsfs.Literal, None)) - self.literals = [self.l_root, self.l_string, self.l_integer, self.l_unused] + self.literals = [self.l_root, self.l_number, self.l_string, self.l_integer, self.l_unused] # predicates - self.p_root = types.Predicate(ns.bsfs.Predicate, None, types.Node(ns.bsfs.Node, None), None, False) + self.p_root = types.ROOT_PREDICATE + self.f_root = types.ROOT_FEATURE self.p_tag = self.p_root.get_child(ns.bse.tag, self.n_ent, self.n_tag, False) self.p_group = self.p_tag.get_child(ns.bse.group, self.n_img, self.n_tag, False) self.p_comment = self.p_root.get_child(ns.bse.comment, self.n_root, self.l_string, True) - self.predicates = [self.p_root, self.p_tag, self.p_group, self.p_comment] + self.predicates = [self.p_root, self.f_root, self.p_tag, self.p_group, self.p_comment] def test_construction(self): + # no args yields a minimal schema + schema = Schema() + self.assertSetEqual(set(schema.nodes()), {self.n_root}) + self.assertSetEqual(set(schema.literals()), {self.l_root, self.l_number}) + self.assertSetEqual(set(schema.predicates()), {self.p_root, self.f_root}) + # nodes and literals are optional schema = Schema(self.predicates) self.assertSetEqual(set(schema.nodes()), {self.n_root, self.n_ent, self.n_img, self.n_tag}) - self.assertSetEqual(set(schema.literals()), {self.l_root, self.l_string}) + self.assertSetEqual(set(schema.literals()), {self.l_root, self.l_string, self.l_number}) self.assertSetEqual(set(schema.predicates()), set(self.predicates)) # predicates, nodes, and literals are respected @@ -101,21 +109,21 @@ class TestSchema(unittest.TestCase): # literals are complete schema = Schema(self.predicates, self.nodes, None) - self.assertSetEqual(set(schema.literals()), {self.l_root, self.l_string}) + self.assertSetEqual(set(schema.literals()), {self.l_root, self.l_string, self.l_number}) schema = Schema(self.predicates, self.nodes, []) - self.assertSetEqual(set(schema.literals()), {self.l_root, self.l_string}) + self.assertSetEqual(set(schema.literals()), {self.l_root, self.l_string, self.l_number}) schema = Schema(self.predicates, self.nodes, [self.l_string]) - self.assertSetEqual(set(schema.literals()), {self.l_root, self.l_string}) + self.assertSetEqual(set(schema.literals()), {self.l_root, self.l_string, self.l_number}) schema = Schema(self.predicates, self.nodes, [self.l_integer]) - self.assertSetEqual(set(schema.literals()), {self.l_root, self.l_string, self.l_integer}) + self.assertSetEqual(set(schema.literals()), {self.l_root, self.l_string, self.l_integer, self.l_number}) schema = Schema(self.predicates, self.nodes, [self.l_integer, self.l_unused]) self.assertSetEqual(set(schema.literals()), set(self.literals)) # predicates are complete schema = Schema([], self.nodes, self.literals) - self.assertSetEqual(set(schema.predicates()), set()) + self.assertSetEqual(set(schema.predicates()), {self.p_root, self.f_root}) schema = Schema([self.p_group], self.nodes, self.literals) - self.assertSetEqual(set(schema.predicates()), {self.p_root, self.p_tag, self.p_group}) + self.assertSetEqual(set(schema.predicates()), {self.p_root, self.f_root, self.p_tag, self.p_group}) schema = Schema([self.p_group, self.p_comment], self.nodes, self.literals) self.assertSetEqual(set(schema.predicates()), set(self.predicates)) @@ -153,21 +161,28 @@ class TestSchema(unittest.TestCase): self.assertRaises(errors.ConsistencyError, Schema, {}, {types.Node(ns.bsfs.Foo, None)}, {types.Node(ns.bsfs.Foo, None)}) self.assertRaises(errors.ConsistencyError, Schema, - {types.Predicate(ns.bsfs.Foo, None, types.Node(ns.bsfs.Node, None), None, False)}, {}, {types.Node(ns.bsfs.Foo, None)}) + {types.Predicate(ns.bsfs.Foo, None, types.Node(ns.bsfs.Node, None), types.ROOT_VERTEX, False)}, {}, {types.Node(ns.bsfs.Foo, None)}) self.assertRaises(errors.ConsistencyError, Schema, - {types.Predicate(ns.bsfs.Foo, None, types.Node(ns.bsfs.Node, None), None, False)}, {types.Node(ns.bsfs.Foo, None)}, {}) + {types.Predicate(ns.bsfs.Foo, None, types.Node(ns.bsfs.Node, None), types.ROOT_VERTEX, False)}, {types.Node(ns.bsfs.Foo, None)}, {}) self.assertRaises(errors.ConsistencyError, Schema, - {types.Predicate(ns.bsfs.Foo, None, types.Node(ns.bsfs.Node, None), None, False)}, {types.Node(ns.bsfs.Foo, None)}, {types.Node(ns.bsfs.Foo, None)}) + {types.Predicate(ns.bsfs.Foo, None, types.Node(ns.bsfs.Node, None), types.ROOT_VERTEX, False)}, {types.Node(ns.bsfs.Foo, None)}, {types.Node(ns.bsfs.Foo, None)}) + def test_str(self): + # string conversion self.assertEqual(str(Schema([])), 'Schema()') self.assertEqual(str(Schema([], [], [])), 'Schema()') self.assertEqual(str(Schema(self.predicates, self.nodes, self.literals)), 'Schema()') - self.assertEqual(repr(Schema([])), 'Schema([], [], [])') - self.assertEqual(repr(Schema([], [], [])), 'Schema([], [], [])') + # repr conversion with only default nodes, literals, and predicates + n = [ns.bsfs.Node] + l = [ns.bsfs.Literal, ns.bsfs.Number] + p = [ns.bsfs.Feature, ns.bsfs.Predicate] + self.assertEqual(repr(Schema()), f'Schema({n}, {l}, {p})') + self.assertEqual(repr(Schema([], [], [])), f'Schema({n}, {l}, {p})') + # repr conversion n = [ns.bsfs.Entity, ns.bsfs.Image, ns.bsfs.Node, ns.bsfs.Tag, ns.bsfs.Unused] - l = [ns.bsfs.Literal, ns.xsd.boolean, ns.xsd.integer, ns.xsd.string] - p = [ns.bse.comment, ns.bse.group, ns.bse.tag, ns.bsfs.Predicate] + l = [ns.bsfs.Literal, ns.bsfs.Number, ns.xsd.boolean, ns.xsd.integer, ns.xsd.string] + p = [ns.bse.comment, ns.bse.group, ns.bse.tag, ns.bsfs.Feature, ns.bsfs.Predicate] self.assertEqual(repr(Schema(self.predicates, self.nodes, self.literals)), f'Schema({n}, {l}, {p})') def test_equality(self): @@ -258,11 +273,11 @@ class TestSchema(unittest.TestCase): self.assertTrue(operator.lt(Schema({self.p_tag}), Schema({self.p_group}))) self.assertTrue(operator.le(Schema({self.p_tag}), Schema({self.p_group}))) # subset considers differences in predicates and literals - self.assertTrue(operator.lt(Schema.Empty(), Schema({self.p_comment}))) + self.assertTrue(operator.lt(Schema(), Schema({self.p_comment}))) # subset considers differences in predicates, nodes, and literals - self.assertTrue(operator.lt(Schema({}), Schema.Empty())) - self.assertTrue(operator.lt(Schema({self.p_tag}), Schema.from_string(self.schema_str))) - self.assertTrue(operator.le(Schema({self.p_tag}), Schema.from_string(self.schema_str))) + self.assertTrue(operator.le(Schema({}), Schema())) + self.assertTrue(operator.lt(Schema({self.p_tag}), from_string(self.schema_str))) + self.assertTrue(operator.le(Schema({self.p_tag}), from_string(self.schema_str))) self.assertFalse(operator.lt(Schema({self.p_comment}), Schema({self.p_tag}))) self.assertFalse(operator.le(Schema({self.p_comment}), Schema({self.p_tag}))) @@ -280,11 +295,11 @@ class TestSchema(unittest.TestCase): self.assertTrue(operator.gt(Schema({self.p_group}), Schema({self.p_tag}))) self.assertTrue(operator.ge(Schema({self.p_group}), Schema({self.p_tag}))) # superset considers differences in predicates and literals - self.assertTrue(operator.gt(Schema({self.p_comment}), Schema.Empty())) + self.assertTrue(operator.gt(Schema({self.p_comment}), Schema())) # superset considers differences in predicates, nodes, and literals - self.assertTrue(operator.gt(Schema.Empty(), Schema({}))) - self.assertTrue(operator.gt(Schema.from_string(self.schema_str), Schema({self.p_tag}))) - self.assertTrue(operator.ge(Schema.from_string(self.schema_str), Schema({self.p_tag}))) + self.assertTrue(operator.ge(Schema(), Schema({}))) + self.assertTrue(operator.gt(from_string(self.schema_str), Schema({self.p_tag}))) + self.assertTrue(operator.ge(from_string(self.schema_str), Schema({self.p_tag}))) self.assertFalse(operator.gt(Schema({self.p_tag}), Schema({self.p_comment}))) self.assertFalse(operator.ge(Schema({self.p_tag}), Schema({self.p_comment}))) @@ -351,26 +366,26 @@ class TestSchema(unittest.TestCase): # difference does not contain predicates from the RHS diff = Schema({self.p_tag, self.p_comment}).diff(Schema({self.p_group})) self.assertSetEqual(set(diff.nodes), set()) - self.assertSetEqual(set(diff.literals), {self.l_root, self.l_string}) + self.assertSetEqual(set(diff.literals), {self.l_string}) self.assertSetEqual(set(diff.predicates), {self.p_comment}) # difference considers extra nodes and literals diff = Schema({self.p_tag}, {self.n_unused}, {self.l_unused}).diff(Schema({self.p_tag})) self.assertSetEqual(set(diff.nodes), {self.n_unused}) - self.assertSetEqual(set(diff.literals), {self.l_root, self.l_unused}) + self.assertSetEqual(set(diff.literals), {self.l_unused}) self.assertSetEqual(set(diff.predicates), set()) # difference considers inconsistent types diff = Schema({self.p_tag}, {self.n_unused}, {self.l_unused}).diff( Schema({self.p_tag}, {types.Node(ns.bsfs.Unused, None)}, {types.Literal(ns.xsd.boolean, None)})) self.assertSetEqual(set(diff.nodes), {self.n_unused}) - self.assertSetEqual(set(diff.literals), {self.l_root, self.l_unused}) + self.assertSetEqual(set(diff.literals), {self.l_unused}) self.assertSetEqual(set(diff.predicates), set()) # __sub__ is an alias for diff diff = Schema({self.p_comment}, {self.n_unused}, {self.l_unused}) - Schema({self.p_group}) self.assertSetEqual(set(diff.nodes), {self.n_unused}) - self.assertSetEqual(set(diff.literals), {self.l_root, self.l_string, self.l_unused}) + self.assertSetEqual(set(diff.literals), {self.l_string, self.l_unused}) self.assertSetEqual(set(diff.predicates), {self.p_comment}) # __sub__ only accepts Schema instances class Foo(): pass @@ -547,196 +562,6 @@ class TestSchema(unittest.TestCase): self.assertFalse(schema.has_predicate(ns.bse.mimetype)) self.assertFalse(schema.has_predicate(self.p_root)) - def test_empty(self): - self.assertEqual(Schema.Empty(), Schema( - [types.Predicate(ns.bsfs.Predicate, None, types.Node(ns.bsfs.Node, None), None, False)], - [types.Node(ns.bsfs.Node, None)], - [types.Literal(ns.bsfs.Literal, None)], - )) - - def test_from_string(self): - # from_string creates a schema - self.assertEqual( - Schema(self.predicates, self.nodes, self.literals), - Schema.from_string(self.schema_str)) - - # schema contains at least the root types - self.assertEqual(Schema.from_string(''), Schema({self.p_root}, {self.n_root}, {self.l_root})) - - # custom example - self.assertEqual( - Schema({types.Predicate(ns.bsfs.Predicate, None, self.n_root, None, False).get_child( - ns.bse.filename, self.n_ent, self.l_string, False)}), - Schema.from_string(''' - prefix rdfs: - prefix xsd: - prefix bsfs: - prefix bse: - - bsfs:Entity rdfs:subClassOf bsfs:Node . - xsd:string rdfs:subClassOf bsfs:Literal . - - bse:filename rdfs:subClassOf bsfs:Predicate ; - rdfs:domain bsfs:Entity ; - rdfs:range xsd:string ; - bsfs:unique "false"^^xsd:boolean . - ''')) - - # all nodes must be defined - self.assertRaises(errors.ConsistencyError, Schema.from_string, ''' - prefix rdfs: - prefix xsd: - prefix bsfs: - prefix bse: - - xsd:string rdfs:subClassOf bsfs:Literal . - - bse:filename rdfs:subClassOf bsfs:Predicate ; - rdfs:domain bsfs:Entity ; - rdfs:range xsd:string ; - bsfs:unique "false"^^xsd:boolean . - ''') - - # all literals must be defined - self.assertRaises(errors.ConsistencyError, Schema.from_string, ''' - prefix rdfs: - prefix xsd: - prefix bsfs: - prefix bse: - - bsfs:Entity rdfs:subClassOf bsfs:Node . - - bse:filename rdfs:subClassOf bsfs:Predicate ; - rdfs:domain bsfs:Entity ; - rdfs:range xsd:string ; - bsfs:unique "false"^^xsd:boolean . - ''') - - # must not have circular dependencies - self.assertRaises(errors.ConsistencyError, Schema.from_string, ''' - prefix rdfs: - prefix bsfs: - bsfs:Entity rdfs:subClassOf bsfs:Node . - # ah, a nice circular dependency - bsfs:Entity rdfs:subClassOf bsfs:Document . - bsfs:Document rdfs:subClassOf bsfs:Entity . - bsfs:PDF rdfs:subClassOf bsfs:Document . - ''') - - # range must be a node or literal - self.assertRaises(errors.ConsistencyError, Schema.from_string, ''' - prefix rdfs: - prefix xsd: - prefix bsfs: - prefix bse: - - bsfs:Entity rdfs:subClassOf bsfs:Node . - - bse:filename rdfs:subClassOf bsfs:Predicate ; - rdfs:domain bsfs:Entity ; - rdfs:range xsd:string ; - bsfs:unique "false"^^xsd:boolean . - ''') - self.assertRaises(errors.ConsistencyError, Schema.from_string, ''' - prefix rdfs: - prefix xsd: - prefix bsfs: - prefix bse: - - bsfs:Entity rdfs:subClassOf bsfs:Node . - - bse:filename rdfs:subClassOf bsfs:Predicate ; - rdfs:domain bsfs:Entity ; - rdfs:range bsfs:Foo ; - bsfs:unique "false"^^xsd:boolean . - ''') - self.assertRaises(errors.ConsistencyError, Schema.from_string, ''' - prefix rdfs: - prefix xsd: - prefix bsfs: - prefix bse: - - bsfs:Entity rdfs:subClassOf bsfs:Node . - - bse:filename rdfs:subClassOf bsfs:Predicate ; - rdfs:domain bsfs:Entity ; - rdfs:range bsfs:Predicate ; - bsfs:unique "false"^^xsd:boolean . - ''') - - # must be consistent - self.assertRaises(errors.ConsistencyError, Schema.from_string, ''' - prefix rdfs: - prefix xsd: - prefix bsfs: - - bsfs:Entity rdfs:subClassOf bsfs:Node . - bsfs:Document rdfs:subClassOf bsfs:Node . - bsfs:Document rdfs:subClassOf bsfs:Entity. - ''') - self.assertRaises(errors.ConsistencyError, Schema.from_string, ''' - prefix rdfs: - prefix xsd: - prefix bsfs: - - xsd:string rdfs:subClassOf bsfs:Literal . - xsd:name rdfs:subClassOf bsfs:Literal . - xsd:name rdfs:subClassOf xsd:string . - ''') - self.assertRaises(errors.ConsistencyError, Schema.from_string, ''' - prefix rdfs: - prefix xsd: - prefix bsfs: - prefix bse: - - bsfs:Entity rdfs:subClassOf bsfs:Node . - - bse:foo rdfs:subClassOf bsfs:Predicate ; - rdfs:domain bsfs:Node ; - rdfs:range bsfs:Node ; - bsfs:unique "false"^^xsd:boolean . - - bse:foo rdfs:subClassOf bsfs:Predicate ; - rdfs:domain bsfs:Entity . - - ''') - self.assertRaises(errors.ConsistencyError, Schema.from_string, ''' - prefix rdfs: - prefix xsd: - prefix bsfs: - prefix bse: - - bsfs:Entity rdfs:subClassOf bsfs:Node . - - bse:foo rdfs:subClassOf bsfs:Predicate ; - rdfs:domain bsfs:Node ; - rdfs:range bsfs:Node ; - bsfs:unique "false"^^xsd:boolean . - - bse:foo rdfs:subClassOf bsfs:Predicate ; - rdfs:range bsfs:Entity . - - ''') - self.assertRaises(errors.ConsistencyError, Schema.from_string, ''' - prefix rdfs: - prefix xsd: - prefix bsfs: - prefix bse: - - bsfs:Entity rdfs:subClassOf bsfs:Node . - - bse:foo rdfs:subClassOf bsfs:Predicate ; - rdfs:domain bsfs:Node ; - rdfs:range bsfs:Node ; - bsfs:unique "false"^^xsd:boolean . - - bse:foo rdfs:subClassOf bsfs:Predicate ; - bsfs:unique "true"^^xsd:boolean . - - ''') - - - ## main ## if __name__ == '__main__': diff --git a/test/schema/test_serialize.py b/test/schema/test_serialize.py new file mode 100644 index 0000000..7392cc0 --- /dev/null +++ b/test/schema/test_serialize.py @@ -0,0 +1,1007 @@ +""" + +Part of the tagit test suite. +A copy of the license is provided with the project. +Author: Matthias Baumgartner, 2022 +""" +# imports +import unittest + +# bsfs imports +from bsfs.namespace import ns +from bsfs.schema import Schema, types +from bsfs.utils import errors, URI + +# objects to test +from bsfs.schema.serialize import from_string, to_string + + +## code ## + +class TestFromString(unittest.TestCase): + + def test_empty(self): + # schema contains at least the root types + self.assertEqual(from_string(''), Schema()) + + + def test_circular_dependency(self): + # must not have circular dependencies + self.assertRaises(errors.ConsistencyError, from_string, ''' + prefix rdfs: + prefix bsfs: + bsfs:Entity rdfs:subClassOf bsfs:Node . + # ah, a nice circular dependency + bsfs:Entity rdfs:subClassOf bsfs:Document . + bsfs:Document rdfs:subClassOf bsfs:Entity . + bsfs:PDF rdfs:subClassOf bsfs:Document . + ''') + + + def test_node(self): + # all nodes must be defined + self.assertRaises(errors.ConsistencyError, from_string, ''' + prefix rdfs: + prefix xsd: + prefix bsfs: + prefix bse: + + xsd:string rdfs:subClassOf bsfs:Literal . + + bse:filename rdfs:subClassOf bsfs:Predicate ; + rdfs:domain bsfs:Entity ; + rdfs:range xsd:string ; + bsfs:unique "false"^^xsd:boolean . + ''') + + # node definitions must be consistent (cannot re-use a node uri) + self.assertRaises(errors.ConsistencyError, from_string, ''' + prefix rdfs: + prefix xsd: + prefix bsfs: + + bsfs:Entity rdfs:subClassOf bsfs:Node . + bsfs:Document rdfs:subClassOf bsfs:Node . + bsfs:Document rdfs:subClassOf bsfs:Entity . # conflicting parent + ''') + + # additional nodes can be defined + n_unused = types.ROOT_NODE.get_child(ns.bsfs.unused) + self.assertEqual(Schema({}, {n_unused}), from_string(''' + prefix rdfs: + prefix xsd: + prefix bsfs: + prefix bse: + + bsfs:unused rdfs:subClassOf bsfs:Node . # unused symbol + ''')) + + # a node can have multiple children + n_ent = types.ROOT_NODE.get_child(ns.bsfs.Entity) + n_tag = types.ROOT_NODE.get_child(ns.bsfs.Tag) + n_doc = n_ent.get_child(ns.bsfs.Document) + n_image = n_ent.get_child(ns.bsfs.Image) + self.assertEqual(Schema({}, {n_ent, n_tag, n_doc, n_image}), from_string(''' + prefix rdfs: + prefix xsd: + prefix bsfs: + prefix bse: + + # nodes inherit from same parent + bsfs:Entity rdfs:subClassOf bsfs:Node . + bsfs:Tag rdfs:subClassOf bsfs:Node . + + # nodes inherit from same parent + bsfs:Document rdfs:subClassOf bsfs:Entity . + bsfs:Image rdfs:subClassOf bsfs:Entity . + ''')) + + # additional nodes can be defined and used + n_ent = types.ROOT_NODE.get_child(ns.bsfs.Entity) + l_string = types.ROOT_LITERAL.get_child(ns.xsd.string) + p_filename = types.ROOT_PREDICATE.get_child(ns.bse.filename, + n_ent, l_string, False) + self.assertEqual(Schema({p_filename}), from_string(''' + prefix rdfs: + prefix xsd: + prefix bsfs: + prefix bse: + + bsfs:Entity rdfs:subClassOf bsfs:Node . + xsd:string rdfs:subClassOf bsfs:Literal . + + bse:filename rdfs:subClassOf bsfs:Predicate ; + rdfs:domain bsfs:Entity ; + rdfs:range xsd:string ; + bsfs:unique "false"^^xsd:boolean . + ''')) + + # nodes can have annotations + self.assertDictEqual(from_string(''' + prefix rdfs: + prefix xsd: + prefix bsfs: + + bsfs:Entity rdfs:subClassOf bsfs:Node . + + ''').node(ns.bsfs.Entity).annotations, {}) + self.assertDictEqual(from_string(''' + prefix rdfs: + prefix xsd: + prefix bsfs: + + bsfs:Entity rdfs:subClassOf bsfs:Node ; + rdfs:label "hello world"^^xsd:string ; + bsfs:foo "1234"^^xsd:integer . + + ''').node(ns.bsfs.Entity).annotations, { + ns.rdfs.label: 'hello world', + ns.bsfs.foo: 1234, + }) + + + def test_literal(self): + # all literals must be defined + self.assertRaises(errors.ConsistencyError, from_string, ''' + prefix rdfs: + prefix xsd: + prefix bsfs: + prefix bse: + + bsfs:Entity rdfs:subClassOf bsfs:Node . + + bse:filename rdfs:subClassOf bsfs:Predicate ; + rdfs:domain bsfs:Entity ; + rdfs:range xsd:string ; # undefined symbol + bsfs:unique "false"^^xsd:boolean . + ''') + + # literal definitions must be consistent (cannot re-use a literal uri) + self.assertRaises(errors.ConsistencyError, from_string, ''' + prefix rdfs: + prefix xsd: + prefix bsfs: + + xsd:string rdfs:subClassOf bsfs:Literal . + xsd:name rdfs:subClassOf bsfs:Literal . + xsd:name rdfs:subClassOf xsd:string . # conflicting parent + ''') + + # additional literals can be defined + l_unused = types.ROOT_LITERAL.get_child(ns.xsd.unused) + self.assertEqual(Schema({}, {}, {l_unused}), from_string(''' + prefix rdfs: + prefix xsd: + prefix bsfs: + prefix bse: + + xsd:unused rdfs:subClassOf bsfs:Literal . # unused symbol + ''')) + + # a literal can have multiple children + l_string = types.ROOT_LITERAL.get_child(ns.xsd.string) + l_integer = types.ROOT_LITERAL.get_child(ns.xsd.integer) + l_unsigned = l_integer.get_child(ns.xsd.unsigned) + l_signed = l_integer.get_child(ns.xsd.signed) + self.assertEqual(Schema({}, {}, {l_string, l_integer, l_unsigned, l_signed}), from_string(''' + prefix rdfs: + prefix xsd: + prefix bsfs: + prefix bse: + + # literals inherit from same parent + xsd:string rdfs:subClassOf bsfs:Literal . + xsd:integer rdfs:subClassOf bsfs:Literal . + + # literals inherit from same parent + xsd:unsigned rdfs:subClassOf xsd:integer . + xsd:signed rdfs:subClassOf xsd:integer . + ''')) + + # additional literals can be defined and used + n_ent = types.ROOT_NODE.get_child(ns.bsfs.Entity) + l_string = types.ROOT_LITERAL.get_child(ns.xsd.string) + p_filename = types.ROOT_PREDICATE.get_child(ns.bse.filename, + n_ent, l_string, False) + self.assertEqual(Schema({p_filename}), from_string(''' + prefix rdfs: + prefix xsd: + prefix bsfs: + prefix bse: + + bsfs:Entity rdfs:subClassOf bsfs:Node . + xsd:string rdfs:subClassOf bsfs:Literal . + + bse:filename rdfs:subClassOf bsfs:Predicate ; + rdfs:domain bsfs:Entity ; + rdfs:range xsd:string ; + bsfs:unique "false"^^xsd:boolean . + ''')) + + # literals can have annotations + self.assertDictEqual(from_string(''' + prefix rdfs: + prefix xsd: + prefix bsfs: + + xsd:string rdfs:subClassOf bsfs:Literal . + + ''').literal(ns.xsd.string).annotations, {}) + self.assertDictEqual(from_string(''' + prefix rdfs: + prefix xsd: + prefix bsfs: + + xsd:string rdfs:subClassOf bsfs:Literal ; + rdfs:label "hello world"^^xsd:string ; + bsfs:foo "1234"^^xsd:integer . + + ''').literal(ns.xsd.string).annotations, { + ns.rdfs.label: 'hello world', + ns.bsfs.foo: 1234, + }) + + + def test_predicate(self): + # domain must be defined + self.assertRaises(errors.ConsistencyError, from_string, ''' + prefix rdfs: + prefix xsd: + prefix bsfs: + prefix bse: + + xsd:string rdfs:subClassOf bsfs:Literal . + + bse:filename rdfs:subClassOf bsfs:Predicate ; + rdfs:domain bsfs:Entity ; # undefined symbol + rdfs:range xsd:string ; + bsfs:unique "false"^^xsd:boolean . + ''') + # domain cannot be a literal + self.assertRaises(errors.ConsistencyError, from_string, ''' + prefix rdfs: + prefix xsd: + prefix bsfs: + prefix bse: + + bsfs:Entity rdfs:subClassOf bsfs:Literal . + xsd:string rdfs:subClassOf bsfs:Literal . + + bse:filename rdfs:subClassOf bsfs:Predicate ; + rdfs:domain bsfs:Entity ; # literal instead of node + rdfs:range xsd:string ; + bsfs:unique "false"^^xsd:boolean . + ''') + + # range must be defined + self.assertRaises(errors.ConsistencyError, from_string, ''' + prefix rdfs: + prefix xsd: + prefix bsfs: + prefix bse: + + bsfs:Entity rdfs:subClassOf bsfs:Node . + + bse:filename rdfs:subClassOf bsfs:Predicate ; + rdfs:domain bsfs:Entity ; + rdfs:range xsd:string ; # undefined symbol + bsfs:unique "false"^^xsd:boolean . + ''') + # range must be defined + self.assertRaises(errors.ConsistencyError, from_string, ''' + prefix rdfs: + prefix xsd: + prefix bsfs: + prefix bse: + + bsfs:Entity rdfs:subClassOf bsfs:Node . + + bse:filename rdfs:subClassOf bsfs:Predicate ; + rdfs:domain bsfs:Entity ; + rdfs:range bsfs:Foo ; # undefined symbol + bsfs:unique "false"^^xsd:boolean . + ''') + # range must be a node or a literal + self.assertRaises(errors.ConsistencyError, from_string, ''' + prefix rdfs: + prefix xsd: + prefix bsfs: + prefix bse: + + bsfs:Entity rdfs:subClassOf bsfs:Node . + + bse:filename rdfs:subClassOf bsfs:Predicate ; + rdfs:domain bsfs:Entity ; + rdfs:range bsfs:Predicate ; # invalid symbol + bsfs:unique "false"^^xsd:boolean . + ''') + + # additional predicates can be defined + n_ent = types.ROOT_NODE.get_child(ns.bsfs.Entity) + l_string = types.ROOT_LITERAL.get_child(ns.xsd.string) + p_comment = types.ROOT_PREDICATE.get_child(ns.bse.comment, domain=n_ent, range=l_string, unique=False) + self.assertEqual(Schema({p_comment}), from_string(''' + prefix rdfs: + prefix xsd: + prefix bsfs: + prefix bse: + + bsfs:Entity rdfs:subClassOf bsfs:Node . + xsd:string rdfs:subClassOf bsfs:Literal . + + bse:comment rdfs:subClassOf bsfs:Predicate ; + rdfs:domain bsfs:Entity ; + rdfs:range xsd:string ; + bsfs:unique "false"^^xsd:boolean . + ''')) + + # predicates inherit properties from parents + n_ent = types.ROOT_NODE.get_child(ns.bsfs.Entity) + l_string = types.ROOT_LITERAL.get_child(ns.xsd.string) + p_annotation = types.ROOT_PREDICATE.get_child(ns.bsfs.Annotation, domain=n_ent, range=l_string) + p_comment = p_annotation.get_child(ns.bse.comment, unique=True) + self.assertEqual(Schema({p_comment}), from_string(''' + prefix rdfs: + prefix xsd: + prefix bsfs: + prefix bse: + + bsfs:Entity rdfs:subClassOf bsfs:Node . + xsd:string rdfs:subClassOf bsfs:Literal . + + bsfs:Annotation rdfs:subClassOf bsfs:Predicate ; + rdfs:domain bsfs:Entity ; + rdfs:range xsd:string . + + bse:comment rdfs:subClassOf bsfs:Annotation ; # inherits domain/range from bsfs:Annotation + bsfs:unique "true"^^xsd:boolean . + ''')) + + # we can define partial predicates (w/o specifying a usable range) + n_ent = types.ROOT_NODE.get_child(ns.bsfs.Entity) + l_string = types.ROOT_LITERAL.get_child(ns.xsd.string) + p_annotation = types.ROOT_PREDICATE.get_child(ns.bsfs.Annotation, domain=n_ent) + p_comment = p_annotation.get_child(ns.bse.comment, range=l_string, unique=False) + self.assertEqual(Schema({p_comment}), from_string(''' + prefix rdfs: + prefix xsd: + prefix bsfs: + prefix bse: + + bsfs:Entity rdfs:subClassOf bsfs:Node . + xsd:string rdfs:subClassOf bsfs:Literal . + + bsfs:Annotation rdfs:subClassOf bsfs:Predicate ; # derive predicate w/o setting range + rdfs:domain bsfs:Entity . + + bse:comment rdfs:subClassOf bsfs:Annotation ; # derived predicate w/ setting range + rdfs:range xsd:string ; + bsfs:unique "false"^^xsd:boolean . + ''')) + + # predicate definition can be split across multiple statements. + # statements can be repeated + n_ent = types.ROOT_NODE.get_child(ns.bsfs.Entity) + p_foo = types.ROOT_PREDICATE.get_child(ns.bse.foo, domain=n_ent, range=types.ROOT_NODE, unique=True) + self.assertEqual(Schema({p_foo}), from_string(''' + prefix rdfs: + prefix xsd: + prefix bsfs: + prefix bse: + + bsfs:Entity rdfs:subClassOf bsfs:Node . + + bse:foo rdfs:subClassOf bsfs:Predicate ; + rdfs:range bsfs:Node ; + bsfs:unique "true"^^xsd:boolean . + + bse:foo rdfs:subClassOf bsfs:Predicate ; + rdfs:domain bsfs:Entity. + ''')) + + # domain must be a subtype of parent's domain + n_ent = types.ROOT_NODE.get_child(ns.bsfs.Entity) + n_image = n_ent.get_child(ns.bsfs.Image) + p_foo = types.ROOT_PREDICATE.get_child(ns.bse.foo, domain=types.ROOT_NODE) + p_bar = p_foo.get_child(ns.bse.bar, domain=n_ent) + p_foobar = p_bar.get_child(ns.bse.foobar, domain=n_image) + self.assertEqual(Schema({p_foobar}), from_string(''' + prefix rdfs: + prefix xsd: + prefix bsfs: + prefix bse: + + bsfs:Entity rdfs:subClassOf bsfs:Node . + bsfs:Image rdfs:subClassOf bsfs:Entity . + + bse:foo rdfs:subClassOf bsfs:Predicate ; + rdfs:domain bsfs:Node . + bse:bar rdfs:subClassOf bse:foo ; + rdfs:domain bsfs:Entity . + bse:foobar rdfs:subClassOf bse:bar ; + rdfs:domain bsfs:Image . + ''')) + self.assertRaises(errors.ConsistencyError, from_string, ''' + prefix rdfs: + prefix xsd: + prefix bsfs: + prefix bse: + + bsfs:Entity rdfs:subClassOf bsfs:Node . + bsfs:Image rdfs:subClassOf bsfs:Entity . + + bse:foo rdfs:subClassOf bsfs:Predicate ; + rdfs:domain bsfs:Image . + bse:bar rdfs:subClassOf bse:foo ; + rdfs:domain bsfs:Entity . + bse:foobar rdfs:subClassOf bse:bar ; + rdfs:domain bsfs:Node . + ''') + + # range must be a subtype of parent's range + n_ent = types.ROOT_NODE.get_child(ns.bsfs.Entity) + n_image = n_ent.get_child(ns.bsfs.Image) + p_foo = types.ROOT_PREDICATE.get_child(ns.bse.foo, range=types.ROOT_NODE) + p_bar = p_foo.get_child(ns.bse.bar, range=n_ent) + p_foobar = p_bar.get_child(ns.bse.foobar, range=n_image) + self.assertEqual(Schema({p_foobar}), from_string(''' + prefix rdfs: + prefix xsd: + prefix bsfs: + prefix bse: + + bsfs:Entity rdfs:subClassOf bsfs:Node . + bsfs:Image rdfs:subClassOf bsfs:Entity . + + bse:foo rdfs:subClassOf bsfs:Predicate ; + rdfs:range bsfs:Node . + bse:bar rdfs:subClassOf bse:foo ; + rdfs:range bsfs:Entity . + bse:foobar rdfs:subClassOf bse:bar ; + rdfs:range bsfs:Image . + ''')) + self.assertRaises(errors.ConsistencyError, from_string, ''' + prefix rdfs: + prefix xsd: + prefix bsfs: + prefix bse: + + bsfs:Entity rdfs:subClassOf bsfs:Node . + bsfs:Image rdfs:subClassOf bsfs:Entity . + + bse:foo rdfs:subClassOf bsfs:Predicate ; + rdfs:range bsfs:Image . + bse:bar rdfs:subClassOf bse:foo ; + rdfs:range bsfs:Entity . + bse:foobar rdfs:subClassOf bse:bar ; + rdfs:range bsfs:Node . + ''') + + # cannot define the same predicate from multiple parents + self.assertRaises(errors.ConsistencyError, from_string, ''' + prefix rdfs: + prefix xsd: + prefix bsfs: + prefix bse: + + bsfs:Entity rdfs:subClassOf bsfs:Node . + bsfs:Annotation rdfs:subClassOf bsfs:Predicate . + + bse:foo rdfs:subClassOf bsfs:Predicate ; + rdfs:domain bsfs:Node ; + rdfs:range bsfs:Node ; + bsfs:unique "false"^^xsd:boolean . + + bse:foo rdfs:subClassOf bsfs:Annotation ; + rdfs:domain bsfs:Node ; + rdfs:range bsfs:Node ; + bsfs:unique "false"^^xsd:boolean . + + ''') + # cannot assign multiple conflicting domains to the same predicate + self.assertRaises(errors.ConsistencyError, from_string, ''' + prefix rdfs: + prefix xsd: + prefix bsfs: + prefix bse: + + bsfs:Entity rdfs:subClassOf bsfs:Node . + + bse:foo rdfs:subClassOf bsfs:Predicate ; + rdfs:domain bsfs:Node ; + rdfs:range bsfs:Node ; + bsfs:unique "false"^^xsd:boolean . + + bse:foo rdfs:subClassOf bsfs:Predicate ; + rdfs:domain bsfs:Entity . # conflicting domain + ''') + # cannot assign multiple conflicting ranges to the same predicate + self.assertRaises(errors.ConsistencyError, from_string, ''' + prefix rdfs: + prefix xsd: + prefix bsfs: + prefix bse: + + bsfs:Entity rdfs:subClassOf bsfs:Node . + + bse:foo rdfs:subClassOf bsfs:Predicate ; + rdfs:domain bsfs:Node ; + rdfs:range bsfs:Node ; + bsfs:unique "false"^^xsd:boolean . + + bse:foo rdfs:subClassOf bsfs:Predicate ; + rdfs:range bsfs:Entity . # conflicting range + ''') + # cannot assign multiple conflicting uniques to the same predicate + self.assertRaises(errors.ConsistencyError, from_string, ''' + prefix rdfs: + prefix xsd: + prefix bsfs: + prefix bse: + + bsfs:Entity rdfs:subClassOf bsfs:Node . + + bse:foo rdfs:subClassOf bsfs:Predicate ; + rdfs:domain bsfs:Node ; + rdfs:range bsfs:Node ; + bsfs:unique "false"^^xsd:boolean . + + bse:foo rdfs:subClassOf bsfs:Predicate ; + bsfs:unique "true"^^xsd:boolean . # conflicting unique + ''') + + # predicates can have annotations + self.assertDictEqual(from_string(''' + prefix rdfs: + prefix xsd: + prefix bsfs: + prefix bse: + + bse:comment rdfs:subClassOf bsfs:Predicate ; + rdfs:range bsfs:Node . + + ''').predicate(ns.bse.comment).annotations, {}) + self.assertDictEqual(from_string(''' + prefix rdfs: + prefix xsd: + prefix bsfs: + prefix bse: + + bse:comment rdfs:subClassOf bsfs:Predicate ; + rdfs:range bsfs:Node ; + rdfs:label "hello world"^^xsd:string ; + bsfs:foo "1234"^^xsd:integer . + + ''').predicate(ns.bse.comment).annotations, { + ns.rdfs.label: 'hello world', + ns.bsfs.foo: 1234, + }) + + + def test_feature(self): + # domain must be defined + self.assertRaises(errors.ConsistencyError, from_string, ''' + prefix rdfs: + prefix xsd: + prefix bsfs: + prefix bse: + + bsfs:Feature rdfs:subClassOf bsfs:Predicate . + bsfs:array rdfs:subClassOf bsfs:Literal . + + bse:colors rdfs:subClassOf bsfs:Feature ; + rdfs:domain bsfs:Entity ; # undefined symbol + rdfs:range bsfs:array ; + bsfs:unique "false"^^xsd:boolean . + ''') + # domain cannot be a literal + self.assertRaises(errors.ConsistencyError, from_string, ''' + prefix rdfs: + prefix xsd: + prefix bsfs: + prefix bse: + + bsfs:Feature rdfs:subClassOf bsfs:Predicate . + bsfs:Entity rdfs:subClassOf bsfs:Literal . + bsfs:array rdfs:subClassOf bsfs:Literal . + + bse:colors rdfs:subClassOf bsfs:Feature ; + rdfs:domain bsfs:Entity ; # literal instead of node + rdfs:range bsfs:array ; + bsfs:unique "false"^^xsd:boolean . + ''') + + # range must be defined + self.assertRaises(errors.ConsistencyError, from_string, ''' + prefix rdfs: + prefix xsd: + prefix bsfs: + prefix bse: + + bsfs:Feature rdfs:subClassOf bsfs:Predicate . + bsfs:Entity rdfs:subClassOf bsfs:Node . + + bse:colors rdfs:subClassOf bsfs:Feature ; + rdfs:domain bsfs:Entity ; + rdfs:range bsfs:array ; # undefined symbol + bsfs:unique "false"^^xsd:boolean . + ''') + # range must be defined + self.assertRaises(errors.ConsistencyError, from_string, ''' + prefix rdfs: + prefix xsd: + prefix bsfs: + prefix bse: + + bsfs:Feature rdfs:subClassOf bsfs:Predicate . + bsfs:Entity rdfs:subClassOf bsfs:Node . + + bse:colors rdfs:subClassOf bsfs:Feature ; + rdfs:domain bsfs:Entity ; + rdfs:range bsfs:Foo ; # undefined symbol + bsfs:unique "false"^^xsd:boolean . + ''') + # range must be a node or a literal + self.assertRaises(errors.ConsistencyError, from_string, ''' + prefix rdfs: + prefix xsd: + prefix bsfs: + prefix bse: + + bsfs:Feature rdfs:subClassOf bsfs:Predicate . + bsfs:Entity rdfs:subClassOf bsfs:Node . + + bse:colors rdfs:subClassOf bsfs:Feature ; + rdfs:domain bsfs:Entity ; + rdfs:range bsfs:Predicate ; # invalid symbol + bsfs:unique "false"^^xsd:boolean . + ''') + + # additional predicates can be defined + n_ent = types.ROOT_NODE.get_child(ns.bsfs.Entity) + l_array = types.ROOT_LITERAL.get_child(ns.bsfs.array) + p_comment = types.ROOT_FEATURE.get_child(ns.bse.colors, domain=n_ent, range=l_array, unique=False) + self.assertEqual(Schema({p_comment}), from_string(''' + prefix rdfs: + prefix xsd: + prefix bsfs: + prefix bse: + + bsfs:Feature rdfs:subClassOf bsfs:Predicate . + bsfs:Entity rdfs:subClassOf bsfs:Node . + bsfs:array rdfs:subClassOf bsfs:Literal . + + bse:colors rdfs:subClassOf bsfs:Feature ; + rdfs:domain bsfs:Entity ; + rdfs:range bsfs:array ; + bsfs:unique "false"^^xsd:boolean . + ''')) + + # features inherit properties from parents + n_ent = types.ROOT_NODE.get_child(ns.bsfs.Entity) + l_array = types.ROOT_LITERAL.get_child(ns.bsfs.array) + l_string = types.ROOT_LITERAL.get_child(ns.xsd.string) + p_annotation = types.ROOT_FEATURE.get_child(ns.bsfs.Annotation, domain=n_ent, range=l_array, + dimension=1234, dtype=ns.xsd.string) + p_comment = p_annotation.get_child(ns.bse.colors, unique=True) + self.assertEqual(Schema({p_comment}), from_string(''' + prefix rdfs: + prefix xsd: + prefix bsfs: + prefix bse: + + bsfs:Feature rdfs:subClassOf bsfs:Predicate . + bsfs:Entity rdfs:subClassOf bsfs:Node . + bsfs:array rdfs:subClassOf bsfs:Literal . + + bsfs:Annotation rdfs:subClassOf bsfs:Feature ; # inherits defaults from bsfs:Feature + rdfs:domain bsfs:Entity ; + rdfs:range bsfs:array ; + bsfs:dimension "1234"^^xsd:integer ; + bsfs:dtype xsd:string . + + bse:colors rdfs:subClassOf bsfs:Annotation ; # inherits domain/range/etc. from bsfs:Annotation + bsfs:unique "true"^^xsd:boolean . # overwrites bsfs:Predicate + ''')) + + # feature definition can be split across multiple statements. + # statements can be repeated + n_ent = types.ROOT_NODE.get_child(ns.bsfs.Entity) + p_foo = types.ROOT_FEATURE.get_child(ns.bse.foo, domain=n_ent, unique=True, + dimension=1234, dtype=ns.bsfs.f32) + self.assertEqual(Schema({p_foo}), from_string(''' + prefix rdfs: + prefix xsd: + prefix bsfs: + prefix bse: + + bsfs:Feature rdfs:subClassOf bsfs:Predicate . + bsfs:Entity rdfs:subClassOf bsfs:Node . + + bse:foo rdfs:subClassOf bsfs:Feature ; + bsfs:unique "true"^^xsd:boolean ; + bsfs:dimension "1234"^^xsd:integer . + + bse:foo rdfs:subClassOf bsfs:Feature ; + rdfs:domain bsfs:Entity ; + bsfs:dtype bsfs:f32 . + ''')) + + # cannot define the same feature from multiple parents + self.assertRaises(errors.ConsistencyError, from_string, ''' + prefix rdfs: + prefix xsd: + prefix bsfs: + prefix bse: + + bsfs:Feature rdfs:subClassOf bsfs:Predicate . + bsfs:Annotation rdfs:subClassOf bsfs:Feature . + bsfs:Entity rdfs:subClassOf bsfs:Node . + + bse:foo rdfs:subClassOf bsfs:Feature ; + rdfs:domain bsfs:Node ; + bsfs:unique "false"^^xsd:boolean . + + bse:foo rdfs:subClassOf bsfs:Annotation ; + rdfs:domain bsfs:Node ; + bsfs:unique "false"^^xsd:boolean . + + ''') + # cannot assign multiple conflicting domains to the same feature + self.assertRaises(errors.ConsistencyError, from_string, ''' + prefix rdfs: + prefix xsd: + prefix bsfs: + prefix bse: + + bsfs:Feature rdfs:subClassOf bsfs:Predicate . + bsfs:Entity rdfs:subClassOf bsfs:Node . + + bse:foo rdfs:subClassOf bsfs:Feature ; + rdfs:domain bsfs:Node ; + bsfs:unique "false"^^xsd:boolean . + + bse:foo rdfs:subClassOf bsfs:Feature ; + rdfs:domain bsfs:Entity . # conflicting domain + ''') + # cannot assign multiple conflicting ranges to the same feature + self.assertRaises(errors.ConsistencyError, from_string, ''' + prefix rdfs: + prefix xsd: + prefix bsfs: + prefix bse: + + bsfs:Feature rdfs:subClassOf bsfs:Predicate . + bsfs:Entity rdfs:subClassOf bsfs:Node . + bsfs:array rdfs:subClassOf bsfs:Literal . + bsfs:large_array rdfs:subClassOf bsfs:array . + bsfs:small_array rdfs:subClassOf bsfs:array . + + bse:foo rdfs:subClassOf bsfs:Feature ; + rdfs:domain bsfs:Node ; + rdfs:range bsfs:large_array ; + bsfs:unique "false"^^xsd:boolean . + + bse:foo rdfs:subClassOf bsfs:Feature ; + rdfs:range bsfs:small_array . # conflicting range + ''') + # cannot assign multiple conflicting uniques to the same feature + self.assertRaises(errors.ConsistencyError, from_string, ''' + prefix rdfs: + prefix xsd: + prefix bsfs: + prefix bse: + + bsfs:Feature rdfs:subClassOf bsfs:Predicate . + bsfs:Entity rdfs:subClassOf bsfs:Node . + + bse:foo rdfs:subClassOf bsfs:Feature ; + rdfs:domain bsfs:Node ; + rdfs:range bsfs:Node ; + bsfs:unique "false"^^xsd:boolean . + + bse:foo rdfs:subClassOf bsfs:Feature ; + bsfs:unique "true"^^xsd:boolean . # conflicting unique + ''') + # cannot assign multiple conflicting dimensions to the same feature + self.assertRaises(errors.ConsistencyError, from_string, ''' + prefix rdfs: + prefix xsd: + prefix bsfs: + prefix bse: + + bsfs:Feature rdfs:subClassOf bsfs:Predicate . + bsfs:Entity rdfs:subClassOf bsfs:Node . + + bse:foo rdfs:subClassOf bsfs:Feature ; + rdfs:domain bsfs:Node ; + bsfs:dimension "1234"^^xsd:integer . + + bse:foo rdfs:subClassOf bsfs:Feature ; + bsfs:dimension "4321"^^xsd:integer . # conflicting dimension + ''') + # cannot assign multiple conflicting dtypes to the same feature + self.assertRaises(errors.ConsistencyError, from_string, ''' + prefix rdfs: + prefix xsd: + prefix bsfs: + prefix bse: + + bsfs:Feature rdfs:subClassOf bsfs:Predicate . + bsfs:Entity rdfs:subClassOf bsfs:Node . + + bse:foo rdfs:subClassOf bsfs:Feature ; + rdfs:domain bsfs:Node ; + bsfs:dtype bsfs:f32 . + + bse:foo rdfs:subClassOf bsfs:Feature ; + bsfs:dtype bsfs:f16 . # conflicting dtype + ''') + # cannot assign multiple conflicting distance metrics to the same feature + self.assertRaises(errors.ConsistencyError, from_string, ''' + prefix rdfs: + prefix xsd: + prefix bsfs: + prefix bse: + + bsfs:Feature rdfs:subClassOf bsfs:Predicate . + bsfs:Entity rdfs:subClassOf bsfs:Node . + + bse:foo rdfs:subClassOf bsfs:Feature ; + rdfs:domain bsfs:Node ; + bsfs:distance bsfs:euclidean . + + bse:foo rdfs:subClassOf bsfs:Feature ; + bsfs:distance bsfs:cosine . # conflicting distance + ''') + + # features can have annotations + self.assertDictEqual(from_string(''' + prefix rdfs: + prefix xsd: + prefix bsfs: + prefix bse: + + bsfs:Feature rdfs:subClassOf bsfs:Predicate . + bse:colors rdfs:subClassOf bsfs:Feature ; + bsfs:dimension "1234"^^xsd:integer . + + ''').predicate(ns.bse.colors).annotations, {}) + self.assertDictEqual(from_string(''' + prefix rdfs: + prefix xsd: + prefix bsfs: + prefix bse: + + bsfs:Feature rdfs:subClassOf bsfs:Predicate . + bse:colors rdfs:subClassOf bsfs:Feature ; + bsfs:dimension "1234"^^xsd:integer ; + rdfs:label "hello world"^^xsd:string ; + bsfs:foo "1234"^^xsd:integer . + + ''').predicate(ns.bse.colors).annotations, { + ns.rdfs.label: 'hello world', + ns.bsfs.foo: 1234, + }) + + + def test_integration(self): + # nodes + n_ent = types.ROOT_NODE.get_child(ns.bsfs.Entity) + n_tag = types.ROOT_NODE.get_child(ns.bsfs.Tag) + n_image = n_ent.get_child(ns.bsfs.Image) + # literals + l_string = types.ROOT_LITERAL.get_child(ns.xsd.string) + l_array = types.ROOT_LITERAL.get_child(ns.bsfs.array) + l_integer = types.ROOT_LITERAL.get_child(ns.xsd.integer) + l_boolean = types.ROOT_LITERAL.get_child(ns.xsd.boolean) + # predicates + p_annotation = types.ROOT_PREDICATE.get_child(ns.bsfs.Annotation) + p_tag = types.ROOT_PREDICATE.get_child(ns.bse.tag, domain=n_ent, range=n_tag) + p_group = p_tag.get_child(ns.bse.group, domain=n_image, unique=True) + p_comment = p_annotation.get_child(ns.bse.comment, range=l_string) + # features + f_colors = types.ROOT_FEATURE.get_child(URI('http://bsfs.ai/schema/Feature/colors_spatial'), + domain=n_ent, range=l_array, unique=True, dtype=ns.bsfs.f16, distance=ns.bsfs.euclidean) + f_colors1234 = f_colors.get_child(URI('http://bsfs.ai/schema/Feature/colors_spatial#1234'), dimension=1024) + f_colors4321 = f_colors.get_child(URI('http://bsfs.ai/schema/Feature/colors_spatial#4321'), dimension=2048) + # schema + ref = Schema( + {p_annotation, p_tag, p_group, p_comment, f_colors, f_colors1234, f_colors4321}, + {n_ent, n_tag, n_image}, + {l_string, l_integer, l_boolean}) + # load from string + gen = from_string(''' + # generic prefixes + prefix rdfs: + prefix xsd: + + # bsfs prefixes + prefix bsfs: + prefix bse: + + # nodes + bsfs:Entity rdfs:subClassOf bsfs:Node ; + rdfs:label "Principal node"^^xsd:string . + bsfs:Tag rdfs:subClassOf bsfs:Node ; + rdfs:label "Tag"^^xsd:string . + bsfs:Image rdfs:subClassOf bsfs:Entity . + + # literals + xsd:string rdfs:subClassOf bsfs:Literal ; + rdfs:label "A sequence of characters"^^xsd:string . + bsfs:array rdfs:subClassOf bsfs:Literal . + xsd:integer rdfs:subClassOf bsfs:Literal . + xsd:boolean rdfs:subClassOf bsfs:Literal . + + # abstract predicates + bsfs:Annotation rdfs:subClassOf bsfs:Predicate ; + rdfs:label "node annotation"^^xsd:string . + bsfs:Feature rdfs:subClassOf bsfs:Predicate . + + # feature instances + rdfs:subClassOf bsfs:Feature ; + rdfs:domain bsfs:Entity ; + rdfs:range bsfs:array ; + bsfs:unique "true"^^xsd:boolean ; + bsfs:dtype bsfs:f16 ; + bsfs:distance bsfs:euclidean ; + # annotations + rdfs:label "ColorsSpatial instances. Dimension depends on instance."^^xsd:string ; + bsfs:first_arg "1234"^^xsd:integer ; + bsfs:second_arg "hello world"^^xsd:string . + + rdfs:subClassOf ; + bsfs:dimension "1024"^^xsd:integer ; + rdfs:label "Main colors spatial instance"^^xsd:string . + + rdfs:subClassOf ; + bsfs:dimension "2048"^^xsd:integer . + + # predicate instances + bse:tag rdfs:subClassOf bsfs:Predicate ; + rdfs:domain bsfs:Entity ; + rdfs:range bsfs:Tag ; + bsfs:unique "false"^^xsd:boolean ; + # annotations + rdfs:label "connect entity to a tag"^^xsd:string . + + bse:group rdfs:subClassOf bse:tag ; # subtype of another predicate + rdfs:domain bsfs:Image ; + bsfs:unique "true"^^xsd:boolean . + + bse:comment rdfs:subClassOf bsfs:Annotation ; # subtype of abstract predicate + rdfs:domain bsfs:Node ; + rdfs:range xsd:string ; + bsfs:unique "false"^^xsd:boolean . + + ''') + # schemas are equal + self.assertEqual(ref, gen) + # check annotations + self.assertDictEqual(gen.node(ns.bsfs.Entity).annotations, {ns.rdfs.label: 'Principal node'}) + self.assertDictEqual(gen.node(ns.bsfs.Tag).annotations, {ns.rdfs.label: 'Tag'}) + self.assertDictEqual(gen.literal(ns.xsd.string).annotations, {ns.rdfs.label: 'A sequence of characters'}) + self.assertDictEqual(gen.predicate(ns.bsfs.Annotation).annotations, {ns.rdfs.label: 'node annotation'}) + self.assertDictEqual(gen.predicate(URI('http://bsfs.ai/schema/Feature/colors_spatial')).annotations, { + ns.rdfs.label: 'ColorsSpatial instances. Dimension depends on instance.', + ns.bsfs.first_arg: 1234, + ns.bsfs.second_arg: 'hello world', + }) + self.assertDictEqual(gen.predicate(URI('http://bsfs.ai/schema/Feature/colors_spatial#1234')).annotations, { + ns.rdfs.label: 'Main colors spatial instance'}) + self.assertDictEqual(gen.predicate(ns.bse.tag).annotations, {ns.rdfs.label: 'connect entity to a tag'}) + + + +class TestToString(unittest.TestCase): + def test_stub(self): + raise NotImplementedError() + + +## main ## + +if __name__ == '__main__': + unittest.main() + +## EOF ## diff --git a/test/schema/test_types.py b/test/schema/test_types.py index 4a49e6e..af47f0d 100644 --- a/test/schema/test_types.py +++ b/test/schema/test_types.py @@ -10,15 +10,17 @@ import unittest # bsfs imports from bsfs.namespace import ns +from bsfs.schema.types import ROOT_PREDICATE, ROOT_VERTEX, ROOT_FEATURE from bsfs.utils import errors # objects to test -from bsfs.schema.types import _Type, _Vertex, Node, Literal, Predicate +from bsfs.schema.types import _Type, _Vertex, Node, Literal, Predicate, Feature ## code ## class TestType(unittest.TestCase): + def test_parents(self): # create some types fst = _Type('First') @@ -31,7 +33,25 @@ class TestType(unittest.TestCase): self.assertListEqual(list(trd.parents()), [snd, fst]) self.assertListEqual(list(frd.parents()), [trd, snd, fst]) - def test_essentials(self): + def test_annotations(self): + # annotations can be empty + self.assertDictEqual(_Type('Foo', None).annotations, {}) + # annotations are stored + self.assertDictEqual(_Type('Foo', None, foo='bar', bar=123).annotations, { + 'foo': 'bar', + 'bar': 123}) + # comparison ignores annotations + self.assertEqual( + _Type('Foo', None, foo='bar', bar='foo'), + _Type('Foo', None, hello='world', foobar=1234)) + self.assertEqual( + hash(_Type('Foo', None, foo='bar', bar='foo')), + hash(_Type('Foo', None, hello='world', foobar=1234))) + # annotations can be passed to get_child + self.assertDictEqual(_Type('First', foo='bar').get_child('Second', bar='foo').annotations, { + 'bar': 'foo'}) + + def test_string_conversion(self): # type w/o parent self.assertEqual(str(_Type('Foo')), '_Type(Foo)') self.assertEqual(repr(_Type('Foo')), '_Type(Foo, None)') @@ -59,6 +79,9 @@ class TestType(unittest.TestCase): # type persists class Foo(_Type): pass self.assertEqual(Foo('First').get_child('Second'), Foo('Second', Foo('First'))) + # annotations are respected + self.assertDictEqual(_Type('First', foo='bar').get_child('Second', bar='foo').annotations, { + 'bar': 'foo'}) def test_equality(self): # equality depends on uri @@ -76,6 +99,13 @@ class TestType(unittest.TestCase): # comparison respects parent self.assertNotEqual(_Type('Foo', _Type('Bar')), _Type('Foo')) self.assertNotEqual(hash(_Type('Foo', _Type('Bar'))), hash(_Type('Foo'))) + # comparison ignores annotations + self.assertEqual( + _Type('Foo', None, foo='bar', bar='foo'), + _Type('Foo', None, hello='world', foobar=1234)) + self.assertEqual( + hash(_Type('Foo', None, foo='bar', bar='foo')), + hash(_Type('Foo', None, hello='world', foobar=1234))) def test_order(self): # create some types. @@ -109,25 +139,40 @@ class TestType(unittest.TestCase): self.assertFalse(bike > bicycle) self.assertFalse(bike >= bicycle) self.assertFalse(bike == bicycle) + + # can compare types along the class hierarchy class Foo(_Type): pass - foo = Foo(bike.uri, bike.parent) - # cannot compare different types - self.assertRaises(TypeError, operator.lt, foo, bike) - self.assertRaises(TypeError, operator.le, foo, bike) - self.assertRaises(TypeError, operator.gt, foo, bike) - self.assertRaises(TypeError, operator.ge, foo, bike) + foo = Foo('Foo', bike) + self.assertTrue(foo < bike) + self.assertTrue(foo <= bike) + self.assertFalse(foo > bike) + self.assertFalse(foo >= bike) # goes both ways - self.assertRaises(TypeError, operator.lt, bike, foo) - self.assertRaises(TypeError, operator.le, bike, foo) - self.assertRaises(TypeError, operator.gt, bike, foo) - self.assertRaises(TypeError, operator.ge, bike, foo) + self.assertFalse(bike < foo) + self.assertFalse(bike <= foo) + self.assertTrue(bike > foo) + self.assertTrue(bike >= foo) + # cannot compare unrelated classes + class Bar(_Type): pass + bar = Bar('Bar', bike) + self.assertRaises(TypeError, operator.lt, foo, bar) + self.assertRaises(TypeError, operator.le, foo, bar) + self.assertRaises(TypeError, operator.gt, foo, bar) + self.assertRaises(TypeError, operator.ge, foo, bar) + # goes both ways + self.assertRaises(TypeError, operator.lt, bar, foo) + self.assertRaises(TypeError, operator.le, bar, foo) + self.assertRaises(TypeError, operator.gt, bar, foo) + self.assertRaises(TypeError, operator.ge, bar, foo) + class TestPredicate(unittest.TestCase): def test_construction(self): # domain must be a node self.assertRaises(TypeError, Predicate, ns.bse.foo, 1234, None, True) self.assertRaises(TypeError, Predicate, ns.bse.foo, None, Literal(ns.bsfs.Foo, None), None, True) - # range must be None, a Literal, or a Node + # range must be a Literal, a Node, or the root Vertex + self.assertRaises(TypeError, Predicate, ns.bse.foo, None, Node(ns.bsfs.Node, None), None, True) self.assertRaises(TypeError, Predicate, ns.bse.foo, None, Node(ns.bsfs.Node, None), 1234, True) self.assertRaises(TypeError, Predicate, ns.bse.foo, None, Node(ns.bsfs.Node, None), _Vertex(ns.bsfs.Foo, None), True) self.assertRaises(TypeError, Predicate, ns.bse.foo, None, Node(ns.bsfs.Node, None), _Type(ns.bsfs.Foo, None), True) @@ -138,54 +183,52 @@ class TestPredicate(unittest.TestCase): n_root = Node(ns.bsfs.Node, None) n_ent = Node(ns.bsfs.Entity, Node(ns.bsfs.Node, None)) n_tag = Node(ns.bsfs.Tag, Node(ns.bsfs.Tag, None)) - root = Predicate( - uri=ns.bsfs.Predicate, - parent=None, + root = ROOT_PREDICATE + tag = Predicate( + uri=ns.bse.tag, + parent=root, domain=n_root, - range=None, + range=n_tag, unique=False, ) # instance is equal to itself - self.assertEqual(root, root) - self.assertEqual(hash(root), hash(root)) + self.assertEqual(tag, tag) + self.assertEqual(hash(tag), hash(tag)) # instance is equal to a clone - self.assertEqual(root, Predicate(ns.bsfs.Predicate, None, n_root, None, False)) - self.assertEqual(hash(root), hash(Predicate(ns.bsfs.Predicate, None, n_root, None, False))) + self.assertEqual(tag, Predicate(ns.bse.tag, root, n_root, n_tag, False)) + self.assertEqual(hash(tag), hash(Predicate(ns.bse.tag, root, n_root, n_tag, False))) # equality respects uri - self.assertNotEqual(root, Predicate(ns.bsfs.Alternative, None, n_root, None, False)) - self.assertNotEqual(hash(root), hash(Predicate(ns.bsfs.Alternative, None, n_root, None, False))) + self.assertNotEqual(tag, Predicate(ns.bsfs.Alternative, root, n_root, n_tag, False)) + self.assertNotEqual(hash(tag), hash(Predicate(ns.bsfs.Alternative, root, n_root, n_tag, False))) # equality respects parent - self.assertNotEqual(root, Predicate(ns.bsfs.Predicate, n_root, n_root, None, False)) - self.assertNotEqual(hash(root), hash(Predicate(ns.bsfs.Predicate, n_root, n_root, None, False))) + self.assertNotEqual(tag, Predicate(ns.bse.tag, n_root, n_root, n_tag, False)) + self.assertNotEqual(hash(tag), hash(Predicate(ns.bse.tag, n_root, n_root, n_tag, False))) # equality respects domain - self.assertNotEqual(root, Predicate(ns.bsfs.Predicate, None, n_ent, None, False)) - self.assertNotEqual(hash(root), hash(Predicate(ns.bsfs.Predicate, None, n_ent, None, False))) + self.assertNotEqual(tag, Predicate(ns.bse.tag, root, n_ent, n_tag, False)) + self.assertNotEqual(hash(tag), hash(Predicate(ns.bse.tag, root, n_ent, n_tag, False))) # equality respects range - self.assertNotEqual(root, Predicate(ns.bsfs.Predicate, None, n_root, n_root, False)) - self.assertNotEqual(hash(root), hash(Predicate(ns.bsfs.Predicate, None, n_root, n_root, False))) + self.assertNotEqual(tag, Predicate(ns.bse.tag, root, n_root, n_root, False)) + self.assertNotEqual(hash(tag), hash(Predicate(ns.bse.tag, root, n_root, n_root, False))) # equality respects unique - self.assertNotEqual(root, Predicate(ns.bsfs.Predicate, None, n_root, None, True)) - self.assertNotEqual(hash(root), hash(Predicate(ns.bsfs.Predicate, None, n_root, None, True))) + self.assertNotEqual(tag, Predicate(ns.bse.tag, root, n_root, n_tag, True)) + self.assertNotEqual(hash(tag), hash(Predicate(ns.bse.tag, root, n_root, n_tag, True))) def test_get_child(self): n_root = Node(ns.bsfs.Node, None) + l_root = Literal(ns.bsfs.Literal, None) n_ent = Node(ns.bsfs.Entity, Node(ns.bsfs.Node, None)) n_tag = Node(ns.bsfs.Tag, Node(ns.bsfs.Tag, None)) - root = Predicate( - uri=ns.bsfs.Predicate, - parent=None, - domain=n_root, - range=None, - unique=False, - ) + root = ROOT_PREDICATE tag = Predicate( - uri=ns.bsfs.Entity, + uri=ns.bse.tag, parent=root, domain=n_ent, range=n_tag, unique=False, ) + # get_child returns Predicate + self.assertIsInstance(tag.get_child(ns.bse.foo), Predicate) # uri is respected self.assertEqual(ns.bse.foo, tag.get_child(ns.bse.foo).uri) # domain is respected @@ -198,10 +241,17 @@ class TestPredicate(unittest.TestCase): self.assertEqual(n_tag, tag.get_child(ns.bse.foo, range=None).range) # unique is respected self.assertTrue(tag.get_child(ns.bse.foo, unique=True).unique) + # annotations are respected + self.assertDictEqual(tag.get_child(ns.bse.foo, foo='bar', bar=123).annotations, { + 'foo': 'bar', + 'bar': 123, + }) # domain is inherited from parent + self.assertEqual(n_root, root.get_child(ns.bse.foo).domain) self.assertEqual(n_ent, tag.get_child(ns.bse.foo).domain) # range is inherited from parent + self.assertEqual(ROOT_VERTEX, root.get_child(ns.bse.foo).range) self.assertEqual(n_tag, tag.get_child(ns.bse.foo).range) # uniqueness is inherited from parent self.assertFalse(tag.get_child(ns.bse.foo).unique) @@ -209,11 +259,118 @@ class TestPredicate(unittest.TestCase): # domain must be subtype of parent's domain self.assertRaises(errors.ConsistencyError, tag.get_child, ns.bse.foo, domain=n_root) self.assertRaises(errors.ConsistencyError, tag.get_child, ns.bse.foo, domain=Node(ns.bsfs.Image, n_root)) - # range cannot be None - self.assertRaises(ValueError, root.get_child, ns.bse.foo) # range must be subtype of parent's range self.assertRaises(errors.ConsistencyError, tag.get_child, ns.bse.foo, range=n_root) self.assertRaises(errors.ConsistencyError, tag.get_child, ns.bse.foo, range=Node(ns.bsfs.Image, n_root)) + self.assertRaises(TypeError, tag.get_child, ns.bse.foo, range=Literal(ns.bsfs.Tag, l_root)) + # range can be subtyped from ROOT_VERTEX to Node or Literal + self.assertEqual(n_root, root.get_child(ns.bse.foo, range=n_root).range) + self.assertEqual(l_root, root.get_child(ns.bse.foo, range=l_root).range) + + +class TestFeature(unittest.TestCase): + def test_construction(self): + n_root = Node(ns.bsfs.Node, None) + l_root = Literal(ns.bsfs.Literal, None) + # dimension, dtype, and distance are respected + feat = Feature(ns.bsfs.Feature, None, n_root, l_root, False, + 1234, ns.bsfs.float, ns.bsfs.euclidean) + self.assertEqual(1234, feat.dimension) + self.assertEqual(ns.bsfs.float, feat.dtype) + self.assertEqual(ns.bsfs.euclidean, feat.distance) + + def test_equality(self): + n_ent = Node(ns.bsfs.Entity, Node(ns.bsfs.Node, None)) + l_array = Literal(ns.bsfs.array, Literal(ns.bsfs.Literal, None)) + colors = Feature( + uri=ns.bse.colors, + parent=ROOT_FEATURE, + domain=n_ent, + range=l_array, + unique=False, + dimension=1234, + dtype=ns.bsfs.float, + distance=ns.bsfs.euclidean, + ) + # instance is equal to itself + self.assertEqual(colors, colors) + self.assertEqual(hash(colors), hash(colors)) + # instance is equal to a clone + self.assertEqual(colors, Feature(ns.bse.colors, ROOT_FEATURE, n_ent, l_array, False, 1234, ns.bsfs.float, ns.bsfs.euclidean)) + self.assertEqual(hash(colors), hash(Feature(ns.bse.colors, ROOT_FEATURE, n_ent, l_array, False, 1234, ns.bsfs.float, ns.bsfs.euclidean))) + # equality respects dimension + self.assertNotEqual(colors, Feature(ns.bse.colors, ROOT_FEATURE, n_ent, l_array, False, 4321, ns.bsfs.float, ns.bsfs.euclidean)) + self.assertNotEqual(hash(colors), hash(Feature(ns.bse.colors, ROOT_FEATURE, n_ent, l_array, False, 4321, ns.bsfs.float, ns.bsfs.euclidean))) + # equality respects dtype + self.assertNotEqual(colors, Feature(ns.bse.colors, ROOT_FEATURE, n_ent, l_array, False, 1234, ns.bsfs.integer, ns.bsfs.euclidean)) + self.assertNotEqual(hash(colors), hash(Feature(ns.bse.colors, ROOT_FEATURE, n_ent, l_array, False, 1234, ns.bsfs.integer, ns.bsfs.euclidean))) + # equality respects distance + self.assertNotEqual(colors, Feature(ns.bse.colors, ROOT_FEATURE, n_ent, l_array, False, 1234, ns.bsfs.float, ns.bsfs.cosine)) + self.assertNotEqual(hash(colors), hash(Feature(ns.bse.colors, ROOT_FEATURE, n_ent, l_array, False, 1234, ns.bsfs.float, ns.bsfs.cosine))) + + def test_get_child(self): + n_root = Node(ns.bsfs.Node, None) + n_ent = Node(ns.bsfs.Entity, n_root) + l_root = Literal(ns.bsfs.Literal, None) + l_array = Literal(ns.bsfs.array, l_root) + colors = Feature( + uri=ns.bse.colors, + parent=ROOT_FEATURE, + domain=n_ent, + range=l_array, + unique=False, + dimension=1234, + dtype=ns.bsfs.float, + distance=ns.bsfs.euclidean, + ) + + # get_child returns Feature + self.assertIsInstance(colors.get_child(ns.bse.foo), Feature) + # uri is respected + self.assertEqual(ns.bse.foo, colors.get_child(ns.bse.foo).uri) + # domain is respected + dom = Node(ns.bsfs.Image, n_ent) + self.assertEqual(dom, colors.get_child(ns.bse.foo, domain=dom).domain) + # range is respected + rng = Literal(ns.bse.foo, l_array) + self.assertEqual(rng, colors.get_child(ns.bse.foo, range=rng).range) + # cannot set range to None + self.assertEqual(l_array, colors.get_child(ns.bse.foo, range=None).range) + # unique is respected + self.assertTrue(colors.get_child(ns.bse.foo, unique=True).unique) + # dimension is respected + self.assertEqual(4321, colors.get_child(ns.bse.foo, dimension=4321).dimension) + # dtype is respected + self.assertEqual(ns.bsfs.integer, colors.get_child(ns.bse.foo, dtype=ns.bsfs.integer).dtype) + # distance is respected + self.assertEqual(ns.bsfs.cosine, colors.get_child(ns.bse.foo, distance=ns.bsfs.cosine).distance) + # annotations are respected + self.assertDictEqual(colors.get_child(ns.bse.foo, foo='bar', bar=123).annotations, { + 'foo': 'bar', + 'bar': 123, + }) + + # domain is inherited from parent + self.assertEqual(n_root, ROOT_FEATURE.get_child(ns.bse.foo).domain) + self.assertEqual(n_ent, colors.get_child(ns.bse.foo).domain) + # range is inherited from parent + self.assertEqual(l_array, colors.get_child(ns.bse.foo).range) + # uniqueness is inherited from parent + self.assertFalse(colors.get_child(ns.bse.foo).unique) + # dimension is inherited from parent + self.assertEqual(1234, colors.get_child(ns.bse.foo).dimension) + # dtype is inherited from parent + self.assertEqual(ns.bsfs.float, colors.get_child(ns.bse.foo).dtype) + # distance is inherited from parent + self.assertEqual(ns.bsfs.euclidean, colors.get_child(ns.bse.foo).distance) + + # domain must be subtype of parent's domain + self.assertRaises(errors.ConsistencyError, colors.get_child, ns.bse.foo, domain=n_root) + self.assertRaises(errors.ConsistencyError, colors.get_child, ns.bse.foo, domain=Node(ns.bsfs.Image, n_root)) + # range must be subtype of parent's range + self.assertRaises(errors.ConsistencyError, colors.get_child, ns.bse.foo, range=Literal(ns.bsfs.Literal, None)) + self.assertRaises(errors.ConsistencyError, colors.get_child, ns.bse.foo, range=Literal(ns.bsfs.foo, Literal(ns.bsfs.Literal, None))) + self.assertRaises(TypeError, colors.get_child, ns.bse.foo, range=Node(ns.bsfs.Tag, n_root)) ## main ## @@ -222,4 +379,3 @@ if __name__ == '__main__': unittest.main() ## EOF ## - -- cgit v1.2.3 From 1ffb815f25b9f7db7b946f9db436974a687cf818 Mon Sep 17 00:00:00 2001 From: Matthias Baumgartner Date: Thu, 12 Jan 2023 08:28:14 +0100 Subject: folder rename due to python import conflict --- test/query/ast/__init__.py | 0 test/query/ast/test_filter_.py | 480 ------------------------------------ test/query/ast_test/__init__.py | 0 test/query/ast_test/test_filter_.py | 480 ++++++++++++++++++++++++++++++++++++ 4 files changed, 480 insertions(+), 480 deletions(-) delete mode 100644 test/query/ast/__init__.py delete mode 100644 test/query/ast/test_filter_.py create mode 100644 test/query/ast_test/__init__.py create mode 100644 test/query/ast_test/test_filter_.py diff --git a/test/query/ast/__init__.py b/test/query/ast/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/test/query/ast/test_filter_.py b/test/query/ast/test_filter_.py deleted file mode 100644 index 4f69bdc..0000000 --- a/test/query/ast/test_filter_.py +++ /dev/null @@ -1,480 +0,0 @@ -""" - -Part of the tagit test suite. -A copy of the license is provided with the project. -Author: Matthias Baumgartner, 2022 -""" -# imports -import unittest - -# bsfs imports -from bsfs.namespace import ns -from bsfs.utils import URI - -# objects to test -from bsfs.query.ast.filter_ import _Expression, FilterExpression, PredicateExpression -from bsfs.query.ast.filter_ import _Branch, Any, All -from bsfs.query.ast.filter_ import _Agg, And, Or -from bsfs.query.ast.filter_ import Not, Has -from bsfs.query.ast.filter_ import _Value, Is, Equals, Substring, StartsWith, EndsWith -from bsfs.query.ast.filter_ import _Bounded, LessThan, GreaterThan -from bsfs.query.ast.filter_ import Predicate, OneOf -from bsfs.query.ast.filter_ import IsIn, IsNotIn - - -## code ## - -class TestExpression(unittest.TestCase): - def test_essentials(self): - # comparison - self.assertEqual(_Expression(), _Expression()) - self.assertEqual(FilterExpression(), FilterExpression()) - self.assertEqual(PredicateExpression(), PredicateExpression()) - self.assertEqual(hash(_Expression()), hash(_Expression())) - self.assertEqual(hash(FilterExpression()), hash(FilterExpression())) - self.assertEqual(hash(PredicateExpression()), hash(PredicateExpression())) - # comparison respects type - self.assertNotEqual(FilterExpression(), _Expression()) - self.assertNotEqual(_Expression(), PredicateExpression()) - self.assertNotEqual(PredicateExpression(), FilterExpression()) - self.assertNotEqual(hash(FilterExpression()), hash(_Expression())) - self.assertNotEqual(hash(_Expression()), hash(PredicateExpression())) - self.assertNotEqual(hash(PredicateExpression()), hash(FilterExpression())) - # string conversion - self.assertEqual(str(_Expression()), '_Expression()') - self.assertEqual(str(FilterExpression()), 'FilterExpression()') - self.assertEqual(str(PredicateExpression()), 'PredicateExpression()') - self.assertEqual(repr(_Expression()), '_Expression()') - self.assertEqual(repr(FilterExpression()), 'FilterExpression()') - self.assertEqual(repr(PredicateExpression()), 'PredicateExpression()') - - -class TestBranch(unittest.TestCase): # _Branch, Any, All - def test_essentials(self): - pred = PredicateExpression() - expr = FilterExpression() - - # comparison respects type - self.assertNotEqual(_Branch(pred, expr), Any(pred, expr)) - self.assertNotEqual(Any(pred, expr), All(pred, expr)) - self.assertNotEqual(All(pred, expr), _Branch(pred, expr)) - self.assertNotEqual(hash(_Branch(pred, expr)), hash(Any(pred, expr))) - self.assertNotEqual(hash(Any(pred, expr)), hash(All(pred, expr))) - self.assertNotEqual(hash(All(pred, expr)), hash(_Branch(pred, expr))) - - for cls in (_Branch, Any, All): - # comparison - self.assertEqual(cls(pred, expr), cls(pred, expr)) - self.assertEqual(hash(cls(pred, expr)), hash(cls(pred, expr))) - # comparison respects predicate - self.assertNotEqual(cls(ns.bse.filename, expr), cls(ns.bse.filesize, expr)) - self.assertNotEqual(hash(cls(ns.bse.filename, expr)), hash(cls(ns.bse.filesize, expr))) - # comparison respects expression - self.assertNotEqual(cls(pred, Equals('hello')), cls(pred, Equals('world'))) - self.assertNotEqual(hash(cls(pred, Equals('hello'))), hash(cls(pred, Equals('world')))) - - # string conversion - self.assertEqual(str(_Branch(pred, expr)), f'_Branch({pred}, {expr})') - self.assertEqual(repr(_Branch(pred, expr)), f'_Branch({pred}, {expr})') - self.assertEqual(str(Any(pred, expr)), f'Any({pred}, {expr})') - self.assertEqual(repr(Any(pred, expr)), f'Any({pred}, {expr})') - self.assertEqual(str(All(pred, expr)), f'All({pred}, {expr})') - self.assertEqual(repr(All(pred, expr)), f'All({pred}, {expr})') - - def test_members(self): - class Foo(): pass - pred = PredicateExpression() - expr = FilterExpression() - - for cls in (_Branch, Any, All): - # predicate returns member - self.assertEqual(cls(PredicateExpression(), expr).predicate, PredicateExpression()) - # can pass an URI - self.assertEqual(cls(ns.bse.filename, expr).predicate, Predicate(ns.bse.filename)) - # can pass a PredicateExpression - self.assertEqual(cls(Predicate(ns.bse.filename), expr).predicate, Predicate(ns.bse.filename)) - # must pass an URI or PredicateExpression - self.assertRaises(TypeError, cls, Foo(), expr) - # expression returns member - self.assertEqual(cls(pred, Equals('hello')).expr, Equals('hello')) - # expression must be a FilterExpression - self.assertRaises(TypeError, cls, ns.bse.filename, 'hello') - self.assertRaises(TypeError, cls, ns.bse.filename, 1234) - self.assertRaises(TypeError, cls, ns.bse.filename, Foo()) - - -class TestAgg(unittest.TestCase): # _Agg, And, Or - def test_essentials(self): - expr = {Equals('hello'), Equals('world')} - - # comparison respects type - self.assertNotEqual(_Agg(expr), And(expr)) - self.assertNotEqual(And(expr), Or(expr)) - self.assertNotEqual(Or(expr), _Agg(expr)) - self.assertNotEqual(hash(_Agg(expr)), hash(And(expr))) - self.assertNotEqual(hash(And(expr)), hash(Or(expr))) - self.assertNotEqual(hash(Or(expr)), hash(_Agg(expr))) - - for cls in (_Agg, And, Or): - # comparison - self.assertEqual(cls(expr), cls(expr)) - self.assertEqual(hash(cls(expr)), hash(cls(expr))) - # comparison respects expression - self.assertNotEqual(cls(expr), cls(Equals('world'))) - self.assertNotEqual(hash(cls(expr)), hash(cls(Equals('world')))) - self.assertNotEqual(cls(Equals('hello')), cls(Equals('world'))) - self.assertNotEqual(hash(cls(Equals('hello'))), hash(cls(Equals('world')))) - - # string conversion - self.assertEqual(str(_Agg(Equals('hello'))), '_Agg({Equals(hello)})') - self.assertEqual(repr(_Agg(Equals('hello'))), '_Agg({Equals(hello)})') - self.assertEqual(str(And(Equals('hello'))), 'And({Equals(hello)})') - self.assertEqual(repr(And(Equals('hello'))), 'And({Equals(hello)})') - self.assertEqual(str(Or(Equals('hello'))), 'Or({Equals(hello)})') - self.assertEqual(repr(Or(Equals('hello'))), 'Or({Equals(hello)})') - - def test_expression(self): - class Foo(): pass - - for cls in (_Agg, And, Or): - # can pass expressions as arguments - self.assertSetEqual(cls(Equals('hello'), Equals('world')).expr, {Equals('hello'), Equals('world')}) - # can pass one expressions as argument - self.assertSetEqual(cls(Equals('hello')).expr, {Equals('hello')}) - # can pass expressions as iterator - self.assertSetEqual(cls(iter((Equals('hello'), Equals('world')))).expr, {Equals('hello'), Equals('world')}) - # can pass expressions as generator - def gen(): - yield Equals('hello') - yield Equals('world') - self.assertSetEqual(cls(gen()).expr, {Equals('hello'), Equals('world')}) - # can pass expressions as list-like - self.assertSetEqual(cls((Equals('hello'), Equals('world'))).expr, {Equals('hello'), Equals('world')}) - # can pass one expression as list-like - self.assertSetEqual(cls([Equals('hello')]).expr, {Equals('hello')}) - # must pass expressions - self.assertRaises(TypeError, cls, Foo(), Foo()) - self.assertRaises(TypeError, cls, [Foo(), Foo()]) - - # iter - self.assertSetEqual(set(iter(cls(Equals('hello'), Equals('world')))), {Equals('hello'), Equals('world')}) - # contains - self.assertIn(Equals('world'), cls(Equals('hello'), Equals('world'))) - self.assertNotIn(Equals('foo'), cls(Equals('hello'), Equals('world'))) - # len - self.assertEqual(len(cls(Equals('hello'), Equals('world'))), 2) - self.assertEqual(len(cls(Equals('hello'), Equals('world'), Equals('foo'))), 3) - - - -class TestNot(unittest.TestCase): - def test_essentials(self): - expr = FilterExpression() - # comparison - self.assertEqual(Not(expr), Not(expr)) - self.assertEqual(hash(Not(expr)), hash(Not(expr))) - # comparison respects type - self.assertNotEqual(Not(expr), FilterExpression()) - self.assertNotEqual(hash(Not(expr)), hash(FilterExpression())) - # comparison respects expression - self.assertNotEqual(Not(Equals('hello')), Not(Equals('world'))) - self.assertNotEqual(hash(Not(Equals('hello'))), hash(Not(Equals('world')))) - # string conversion - self.assertEqual(str(Not(Equals('hello'))), 'Not(Equals(hello))') - self.assertEqual(repr(Not(Equals('hello'))), 'Not(Equals(hello))') - - def test_expression(self): - # Not requires an expression argument - self.assertRaises(TypeError, Not) - # expression must be a FilterExpression - self.assertRaises(TypeError, Not, 'hello') - self.assertRaises(TypeError, Not, 1234) - self.assertRaises(TypeError, Not, Predicate(ns.bse.filesize)) - # member returns expression - self.assertEqual(Not(Equals('hello')).expr, Equals('hello')) - - -class TestHas(unittest.TestCase): - def test_essentials(self): - pred = PredicateExpression() - count = FilterExpression() - # comparison - self.assertEqual(Has(pred, count), Has(pred, count)) - self.assertEqual(hash(Has(pred, count)), hash(Has(pred, count))) - # comparison respects type - self.assertNotEqual(Has(pred, count), FilterExpression()) - self.assertNotEqual(hash(Has(pred, count)), hash(FilterExpression())) - # comparison respects predicate - self.assertNotEqual(Has(pred, count), Has(Predicate(ns.bse.filesize), count)) - self.assertNotEqual(hash(Has(pred, count)), hash(Has(Predicate(ns.bse.filesize), count))) - # comparison respects count - self.assertNotEqual(Has(pred, count), Has(pred, LessThan(5))) - self.assertNotEqual(hash(Has(pred, count)), hash(Has(pred, LessThan(5)))) - # string conversion - self.assertEqual(str(Has(Predicate(ns.bse.filesize), LessThan(5))), - f'Has(Predicate({ns.bse.filesize}, False), LessThan(5.0, True))') - self.assertEqual(repr(Has(Predicate(ns.bse.filesize), LessThan(5))), - f'Has(Predicate({ns.bse.filesize}, False), LessThan(5.0, True))') - - def test_members(self): - pred = PredicateExpression() - count = FilterExpression() - # member returns expression - # predicate must be an URI or a PredicateExpression - self.assertEqual(Has(ns.bse.filesize, count).predicate, Predicate(ns.bse.filesize)) - self.assertEqual(Has(Predicate(ns.bse.filesize), count).predicate, Predicate(ns.bse.filesize)) - self.assertRaises(TypeError, Has, 1234, FilterExpression()) - self.assertRaises(TypeError, Has, FilterExpression(), FilterExpression()) - # member returns count - # count must be None, an integer, or a FilterExpression - self.assertEqual(Has(pred).count, GreaterThan(1, False)) - self.assertEqual(Has(pred, LessThan(5)).count, LessThan(5)) - self.assertEqual(Has(pred, 5).count, Equals(5)) - self.assertRaises(TypeError, Has, pred, 'hello') - self.assertRaises(TypeError, Has, pred, Predicate(ns.bse.filesize)) - - - -class TestValue(unittest.TestCase): - def test_essentials(self): - # comparison respects type - self.assertNotEqual(_Value('hello'), Equals('hello')) - self.assertNotEqual(Equals('hello'), Is('hello')) - self.assertNotEqual(Is('hello'), Substring('hello')) - self.assertNotEqual(Substring('hello'), StartsWith('hello')) - self.assertNotEqual(StartsWith('hello'), EndsWith('hello')) - self.assertNotEqual(EndsWith('hello'), _Value('hello')) - self.assertNotEqual(hash(_Value('hello')), hash(Equals('hello'))) - self.assertNotEqual(hash(Equals('hello')), hash(Is('hello'))) - self.assertNotEqual(hash(Is('hello')), hash(Substring('hello'))) - self.assertNotEqual(hash(Substring('hello')), hash(StartsWith('hello'))) - self.assertNotEqual(hash(StartsWith('hello')), hash(EndsWith('hello'))) - self.assertNotEqual(hash(EndsWith('hello')), hash(_Value('hello'))) - - for cls in (_Value, Is, Equals, Substring, StartsWith, EndsWith): - # comparison - self.assertEqual(cls('hello'), cls('hello')) - self.assertEqual(hash(cls('hello')), hash(cls('hello'))) - # comparison respects value - self.assertNotEqual(cls('hello'), cls('world')) - self.assertNotEqual(hash(cls('hello')), hash(cls('world'))) - - # string conversion - self.assertEqual(str(_Value('hello')), '_Value(hello)') - self.assertEqual(repr(_Value('hello')), '_Value(hello)') - self.assertEqual(str(Is('hello')), 'Is(hello)') - self.assertEqual(repr(Is('hello')), 'Is(hello)') - self.assertEqual(str(Equals('hello')), 'Equals(hello)') - self.assertEqual(repr(Equals('hello')), 'Equals(hello)') - self.assertEqual(str(Substring('hello')), 'Substring(hello)') - self.assertEqual(repr(Substring('hello')), 'Substring(hello)') - self.assertEqual(str(StartsWith('hello')), 'StartsWith(hello)') - self.assertEqual(repr(StartsWith('hello')), 'StartsWith(hello)') - self.assertEqual(str(EndsWith('hello')), 'EndsWith(hello)') - self.assertEqual(repr(EndsWith('hello')), 'EndsWith(hello)') - - def test_value(self): - class Foo(): pass - for cls in (_Value, Is, Equals, Substring, StartsWith, EndsWith): - # value can be anything - # value returns member - f = Foo() - self.assertEqual(cls('hello').value, 'hello') - self.assertEqual(cls(1234).value, 1234) - self.assertEqual(cls(f).value, f) - - -class TestBounded(unittest.TestCase): - def test_essentials(self): - # comparison respects type - self.assertNotEqual(_Bounded(1234), LessThan(1234)) - self.assertNotEqual(LessThan(1234), GreaterThan(1234)) - self.assertNotEqual(GreaterThan(1234), _Bounded(1234)) - self.assertNotEqual(hash(_Bounded(1234)), hash(LessThan(1234))) - self.assertNotEqual(hash(LessThan(1234)), hash(GreaterThan(1234))) - self.assertNotEqual(hash(GreaterThan(1234)), hash(_Bounded(1234))) - - for cls in (_Bounded, LessThan, GreaterThan): - # comparison - self.assertEqual(cls(1234), cls(1234)) - self.assertEqual(hash(cls(1234)), hash(cls(1234))) - # comparison respects threshold - self.assertNotEqual(cls(1234), cls(4321)) - self.assertNotEqual(hash(cls(1234)), hash(cls(4321))) - # comparison respects strict - self.assertNotEqual(cls(1234, True), cls(1234, False)) - self.assertNotEqual(hash(cls(1234, True)), hash(cls(1234, False))) - - # string conversion - self.assertEqual(str(_Bounded(1234, False)), '_Bounded(1234.0, False)') - self.assertEqual(repr(_Bounded(1234, False)), '_Bounded(1234.0, False)') - self.assertEqual(str(LessThan(1234, False)), 'LessThan(1234.0, False)') - self.assertEqual(repr(LessThan(1234, False)), 'LessThan(1234.0, False)') - self.assertEqual(str(GreaterThan(1234, False)), 'GreaterThan(1234.0, False)') - self.assertEqual(repr(GreaterThan(1234, False)), 'GreaterThan(1234.0, False)') - - def test_members(self): - class Foo(): pass - for cls in (_Bounded, LessThan, GreaterThan): - # threshold becomes float - self.assertEqual(cls(1.234).threshold, 1.234) - self.assertEqual(cls(1234).threshold, 1234.0) - self.assertEqual(cls('1234').threshold, 1234) - self.assertRaises(TypeError, cls, Foo()) - # strict becomes bool - self.assertEqual(cls(1234, True).strict, True) - self.assertEqual(cls(1234, False).strict, False) - self.assertEqual(cls(1234, Foo()).strict, True) - - -class TestPredicate(unittest.TestCase): - def test_essentials(self): - # comparison - self.assertEqual(Predicate(ns.bse.filesize), Predicate(ns.bse.filesize)) - self.assertEqual(hash(Predicate(ns.bse.filesize)), hash(Predicate(ns.bse.filesize))) - # comparison respects type - self.assertNotEqual(Predicate(ns.bse.filesize), PredicateExpression()) - self.assertNotEqual(hash(Predicate(ns.bse.filesize)), hash(PredicateExpression())) - # comparison respects predicate - self.assertNotEqual(Predicate(ns.bse.filesize), Predicate(ns.bse.filename)) - self.assertNotEqual(hash(Predicate(ns.bse.filesize)), hash(Predicate(ns.bse.filename))) - # comparison respects reverse - self.assertNotEqual(Predicate(ns.bse.filesize, True), Predicate(ns.bse.filesize, False)) - self.assertNotEqual(hash(Predicate(ns.bse.filesize, True)), hash(Predicate(ns.bse.filesize, False))) - # string conversion - self.assertEqual(str(Predicate(ns.bse.filesize)), f'Predicate({ns.bse.filesize}, False)') - self.assertEqual(str(Predicate(ns.bse.filesize, True)), - f'Predicate({ns.bse.filesize}, True)') - self.assertEqual(repr(Predicate(ns.bse.filesize)), f'Predicate({ns.bse.filesize}, False)') - self.assertEqual(repr(Predicate(ns.bse.filesize, True)), - f'Predicate({ns.bse.filesize}, True)') - - def test_members(self): - # member returns predicate - # predicate must be an URI - self.assertEqual(Predicate(ns.bse.filesize).predicate, ns.bse.filesize) - self.assertEqual(Predicate(URI('hello world')).predicate, URI('hello world')) - self.assertRaises(TypeError, Predicate, 1234) - self.assertRaises(TypeError, Predicate, FilterExpression()) - self.assertRaises(TypeError, Predicate, FilterExpression()) - # reverse becomes a boolean - self.assertEqual(Predicate(ns.bse.filesize, True).reverse, True) - self.assertEqual(Predicate(ns.bse.filesize, False).reverse, False) - self.assertEqual(Predicate(ns.bse.filesize, 'abc').reverse, True) - - -class TestOneOf(unittest.TestCase): - def test_essentials(self): - expr = {Predicate(ns.bse.filename), Predicate(ns.bse.filesize)} - # comparison - self.assertEqual(OneOf(expr), OneOf(expr)) - self.assertEqual(hash(OneOf(expr)), hash(OneOf(expr))) - # comparison respects type - self.assertNotEqual(OneOf(expr), PredicateExpression()) - self.assertNotEqual(hash(OneOf(expr)), hash(PredicateExpression())) - # comparison respects expression - self.assertNotEqual(OneOf(expr), OneOf(Predicate(ns.bse.filename))) - self.assertNotEqual(hash(OneOf(expr)), hash(OneOf(Predicate(ns.bse.filename)))) - # string conversion - self.assertEqual(str(OneOf(Predicate(ns.bse.filesize))), - f'OneOf({{Predicate({ns.bse.filesize}, False)}})') - self.assertEqual(repr(OneOf(Predicate(ns.bse.filesize))), - f'OneOf({{Predicate({ns.bse.filesize}, False)}})') - - def test_expression(self): - class Foo(): pass - # can pass expressions as arguments - self.assertSetEqual(OneOf(Predicate(ns.bse.filesize), Predicate(ns.bse.filename)).expr, - {Predicate(ns.bse.filesize), Predicate(ns.bse.filename)}) - # can pass one expressions as argument - self.assertSetEqual(OneOf(Predicate(ns.bse.filesize)).expr, - {Predicate(ns.bse.filesize)}) - # can pass expressions as iterator - self.assertSetEqual(OneOf(iter((Predicate(ns.bse.filesize), Predicate(ns.bse.filename)))).expr, - {Predicate(ns.bse.filesize), Predicate(ns.bse.filename)}) - # can pass expressions as generator - def gen(): - yield Predicate(ns.bse.filesize) - yield Predicate(ns.bse.filename) - self.assertSetEqual(OneOf(gen()).expr, - {Predicate(ns.bse.filesize), Predicate(ns.bse.filename)}) - # can pass expressions as list-like - self.assertSetEqual(OneOf((Predicate(ns.bse.filesize), Predicate(ns.bse.filename))).expr, - {Predicate(ns.bse.filesize), Predicate(ns.bse.filename)}) - # can pass one expression as list-like - self.assertSetEqual(OneOf([Predicate(ns.bse.filesize)]).expr, - {Predicate(ns.bse.filesize)}) - # must pass expressions - self.assertRaises(TypeError, OneOf, Foo(), Foo()) - self.assertRaises(TypeError, OneOf, [Foo(), Foo()]) - # must pass at least one expression - self.assertRaises(AttributeError, OneOf) - - # iter - self.assertSetEqual(set(iter(OneOf(Predicate(ns.bse.filesize), Predicate(ns.bse.filename)))), - {Predicate(ns.bse.filesize), Predicate(ns.bse.filename)}) - # contains - self.assertIn(Predicate(ns.bse.filesize), - OneOf(Predicate(ns.bse.filesize), Predicate(ns.bse.filename))) - self.assertNotIn(Predicate(ns.bse.tag), - OneOf(Predicate(ns.bse.filesize), Predicate(ns.bse.filename))) - # len - self.assertEqual(len(OneOf(Predicate(ns.bse.filesize), Predicate(ns.bse.filename))), 2) - self.assertEqual(len(OneOf(Predicate(ns.bse.filesize), Predicate(ns.bse.filename), Predicate(ns.bse.tag))), 3) - - - def testIsIn(self): - # can pass expressions as arguments - self.assertEqual(IsIn('http://example.com/entity#1234', 'http://example.com/entity#4321'), - Or(Is('http://example.com/entity#1234'), Is('http://example.com/entity#4321'))) - # can pass one expression as argument - self.assertEqual(IsIn('http://example.com/entity#1234'), - Or(Is('http://example.com/entity#1234'))) - # can pass expressions as iterator - self.assertEqual(IsIn(iter(('http://example.com/entity#1234', 'http://example.com/entity#4321'))), - Or(Is('http://example.com/entity#1234'), Is('http://example.com/entity#4321'))) - # can pass expressions as generator - def gen(): - yield 'http://example.com/entity#1234' - yield 'http://example.com/entity#4321' - self.assertEqual(IsIn(gen()), - Or(Is('http://example.com/entity#1234'), Is('http://example.com/entity#4321'))) - # can pass expressions as list-like - self.assertEqual(IsIn(['http://example.com/entity#1234', 'http://example.com/entity#4321']), - Or(Is('http://example.com/entity#1234'), Is('http://example.com/entity#4321'))) - # can pass one expression as list-like - self.assertEqual(IsIn(['http://example.com/entity#1234']), - Or(Is('http://example.com/entity#1234'))) - - - def testIsNotIn(self): - # can pass expressions as arguments - self.assertEqual(IsNotIn('http://example.com/entity#1234', 'http://example.com/entity#4321'), - Not(Or(Is('http://example.com/entity#1234'), Is('http://example.com/entity#4321')))) - # can pass one expression as argument - self.assertEqual(IsNotIn('http://example.com/entity#1234'), - Not(Or(Is('http://example.com/entity#1234')))) - # can pass expressions as iterator - self.assertEqual(IsNotIn(iter(('http://example.com/entity#1234', 'http://example.com/entity#4321'))), - Not(Or(Is('http://example.com/entity#1234'), Is('http://example.com/entity#4321')))) - # can pass expressions as generator - def gen(): - yield 'http://example.com/entity#1234' - yield 'http://example.com/entity#4321' - self.assertEqual(IsNotIn(gen()), - Not(Or(Is('http://example.com/entity#1234'), Is('http://example.com/entity#4321')))) - # can pass expressions as list-like - self.assertEqual(IsNotIn(['http://example.com/entity#1234', 'http://example.com/entity#4321']), - Not(Or(Is('http://example.com/entity#1234'), Is('http://example.com/entity#4321')))) - # can pass one expression as list-like - self.assertEqual(IsNotIn(['http://example.com/entity#1234']), - Not(Or(Is('http://example.com/entity#1234')))) - - - -## main ## - -if __name__ == '__main__': - unittest.main() - -## EOF ## diff --git a/test/query/ast_test/__init__.py b/test/query/ast_test/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/test/query/ast_test/test_filter_.py b/test/query/ast_test/test_filter_.py new file mode 100644 index 0000000..4f69bdc --- /dev/null +++ b/test/query/ast_test/test_filter_.py @@ -0,0 +1,480 @@ +""" + +Part of the tagit test suite. +A copy of the license is provided with the project. +Author: Matthias Baumgartner, 2022 +""" +# imports +import unittest + +# bsfs imports +from bsfs.namespace import ns +from bsfs.utils import URI + +# objects to test +from bsfs.query.ast.filter_ import _Expression, FilterExpression, PredicateExpression +from bsfs.query.ast.filter_ import _Branch, Any, All +from bsfs.query.ast.filter_ import _Agg, And, Or +from bsfs.query.ast.filter_ import Not, Has +from bsfs.query.ast.filter_ import _Value, Is, Equals, Substring, StartsWith, EndsWith +from bsfs.query.ast.filter_ import _Bounded, LessThan, GreaterThan +from bsfs.query.ast.filter_ import Predicate, OneOf +from bsfs.query.ast.filter_ import IsIn, IsNotIn + + +## code ## + +class TestExpression(unittest.TestCase): + def test_essentials(self): + # comparison + self.assertEqual(_Expression(), _Expression()) + self.assertEqual(FilterExpression(), FilterExpression()) + self.assertEqual(PredicateExpression(), PredicateExpression()) + self.assertEqual(hash(_Expression()), hash(_Expression())) + self.assertEqual(hash(FilterExpression()), hash(FilterExpression())) + self.assertEqual(hash(PredicateExpression()), hash(PredicateExpression())) + # comparison respects type + self.assertNotEqual(FilterExpression(), _Expression()) + self.assertNotEqual(_Expression(), PredicateExpression()) + self.assertNotEqual(PredicateExpression(), FilterExpression()) + self.assertNotEqual(hash(FilterExpression()), hash(_Expression())) + self.assertNotEqual(hash(_Expression()), hash(PredicateExpression())) + self.assertNotEqual(hash(PredicateExpression()), hash(FilterExpression())) + # string conversion + self.assertEqual(str(_Expression()), '_Expression()') + self.assertEqual(str(FilterExpression()), 'FilterExpression()') + self.assertEqual(str(PredicateExpression()), 'PredicateExpression()') + self.assertEqual(repr(_Expression()), '_Expression()') + self.assertEqual(repr(FilterExpression()), 'FilterExpression()') + self.assertEqual(repr(PredicateExpression()), 'PredicateExpression()') + + +class TestBranch(unittest.TestCase): # _Branch, Any, All + def test_essentials(self): + pred = PredicateExpression() + expr = FilterExpression() + + # comparison respects type + self.assertNotEqual(_Branch(pred, expr), Any(pred, expr)) + self.assertNotEqual(Any(pred, expr), All(pred, expr)) + self.assertNotEqual(All(pred, expr), _Branch(pred, expr)) + self.assertNotEqual(hash(_Branch(pred, expr)), hash(Any(pred, expr))) + self.assertNotEqual(hash(Any(pred, expr)), hash(All(pred, expr))) + self.assertNotEqual(hash(All(pred, expr)), hash(_Branch(pred, expr))) + + for cls in (_Branch, Any, All): + # comparison + self.assertEqual(cls(pred, expr), cls(pred, expr)) + self.assertEqual(hash(cls(pred, expr)), hash(cls(pred, expr))) + # comparison respects predicate + self.assertNotEqual(cls(ns.bse.filename, expr), cls(ns.bse.filesize, expr)) + self.assertNotEqual(hash(cls(ns.bse.filename, expr)), hash(cls(ns.bse.filesize, expr))) + # comparison respects expression + self.assertNotEqual(cls(pred, Equals('hello')), cls(pred, Equals('world'))) + self.assertNotEqual(hash(cls(pred, Equals('hello'))), hash(cls(pred, Equals('world')))) + + # string conversion + self.assertEqual(str(_Branch(pred, expr)), f'_Branch({pred}, {expr})') + self.assertEqual(repr(_Branch(pred, expr)), f'_Branch({pred}, {expr})') + self.assertEqual(str(Any(pred, expr)), f'Any({pred}, {expr})') + self.assertEqual(repr(Any(pred, expr)), f'Any({pred}, {expr})') + self.assertEqual(str(All(pred, expr)), f'All({pred}, {expr})') + self.assertEqual(repr(All(pred, expr)), f'All({pred}, {expr})') + + def test_members(self): + class Foo(): pass + pred = PredicateExpression() + expr = FilterExpression() + + for cls in (_Branch, Any, All): + # predicate returns member + self.assertEqual(cls(PredicateExpression(), expr).predicate, PredicateExpression()) + # can pass an URI + self.assertEqual(cls(ns.bse.filename, expr).predicate, Predicate(ns.bse.filename)) + # can pass a PredicateExpression + self.assertEqual(cls(Predicate(ns.bse.filename), expr).predicate, Predicate(ns.bse.filename)) + # must pass an URI or PredicateExpression + self.assertRaises(TypeError, cls, Foo(), expr) + # expression returns member + self.assertEqual(cls(pred, Equals('hello')).expr, Equals('hello')) + # expression must be a FilterExpression + self.assertRaises(TypeError, cls, ns.bse.filename, 'hello') + self.assertRaises(TypeError, cls, ns.bse.filename, 1234) + self.assertRaises(TypeError, cls, ns.bse.filename, Foo()) + + +class TestAgg(unittest.TestCase): # _Agg, And, Or + def test_essentials(self): + expr = {Equals('hello'), Equals('world')} + + # comparison respects type + self.assertNotEqual(_Agg(expr), And(expr)) + self.assertNotEqual(And(expr), Or(expr)) + self.assertNotEqual(Or(expr), _Agg(expr)) + self.assertNotEqual(hash(_Agg(expr)), hash(And(expr))) + self.assertNotEqual(hash(And(expr)), hash(Or(expr))) + self.assertNotEqual(hash(Or(expr)), hash(_Agg(expr))) + + for cls in (_Agg, And, Or): + # comparison + self.assertEqual(cls(expr), cls(expr)) + self.assertEqual(hash(cls(expr)), hash(cls(expr))) + # comparison respects expression + self.assertNotEqual(cls(expr), cls(Equals('world'))) + self.assertNotEqual(hash(cls(expr)), hash(cls(Equals('world')))) + self.assertNotEqual(cls(Equals('hello')), cls(Equals('world'))) + self.assertNotEqual(hash(cls(Equals('hello'))), hash(cls(Equals('world')))) + + # string conversion + self.assertEqual(str(_Agg(Equals('hello'))), '_Agg({Equals(hello)})') + self.assertEqual(repr(_Agg(Equals('hello'))), '_Agg({Equals(hello)})') + self.assertEqual(str(And(Equals('hello'))), 'And({Equals(hello)})') + self.assertEqual(repr(And(Equals('hello'))), 'And({Equals(hello)})') + self.assertEqual(str(Or(Equals('hello'))), 'Or({Equals(hello)})') + self.assertEqual(repr(Or(Equals('hello'))), 'Or({Equals(hello)})') + + def test_expression(self): + class Foo(): pass + + for cls in (_Agg, And, Or): + # can pass expressions as arguments + self.assertSetEqual(cls(Equals('hello'), Equals('world')).expr, {Equals('hello'), Equals('world')}) + # can pass one expressions as argument + self.assertSetEqual(cls(Equals('hello')).expr, {Equals('hello')}) + # can pass expressions as iterator + self.assertSetEqual(cls(iter((Equals('hello'), Equals('world')))).expr, {Equals('hello'), Equals('world')}) + # can pass expressions as generator + def gen(): + yield Equals('hello') + yield Equals('world') + self.assertSetEqual(cls(gen()).expr, {Equals('hello'), Equals('world')}) + # can pass expressions as list-like + self.assertSetEqual(cls((Equals('hello'), Equals('world'))).expr, {Equals('hello'), Equals('world')}) + # can pass one expression as list-like + self.assertSetEqual(cls([Equals('hello')]).expr, {Equals('hello')}) + # must pass expressions + self.assertRaises(TypeError, cls, Foo(), Foo()) + self.assertRaises(TypeError, cls, [Foo(), Foo()]) + + # iter + self.assertSetEqual(set(iter(cls(Equals('hello'), Equals('world')))), {Equals('hello'), Equals('world')}) + # contains + self.assertIn(Equals('world'), cls(Equals('hello'), Equals('world'))) + self.assertNotIn(Equals('foo'), cls(Equals('hello'), Equals('world'))) + # len + self.assertEqual(len(cls(Equals('hello'), Equals('world'))), 2) + self.assertEqual(len(cls(Equals('hello'), Equals('world'), Equals('foo'))), 3) + + + +class TestNot(unittest.TestCase): + def test_essentials(self): + expr = FilterExpression() + # comparison + self.assertEqual(Not(expr), Not(expr)) + self.assertEqual(hash(Not(expr)), hash(Not(expr))) + # comparison respects type + self.assertNotEqual(Not(expr), FilterExpression()) + self.assertNotEqual(hash(Not(expr)), hash(FilterExpression())) + # comparison respects expression + self.assertNotEqual(Not(Equals('hello')), Not(Equals('world'))) + self.assertNotEqual(hash(Not(Equals('hello'))), hash(Not(Equals('world')))) + # string conversion + self.assertEqual(str(Not(Equals('hello'))), 'Not(Equals(hello))') + self.assertEqual(repr(Not(Equals('hello'))), 'Not(Equals(hello))') + + def test_expression(self): + # Not requires an expression argument + self.assertRaises(TypeError, Not) + # expression must be a FilterExpression + self.assertRaises(TypeError, Not, 'hello') + self.assertRaises(TypeError, Not, 1234) + self.assertRaises(TypeError, Not, Predicate(ns.bse.filesize)) + # member returns expression + self.assertEqual(Not(Equals('hello')).expr, Equals('hello')) + + +class TestHas(unittest.TestCase): + def test_essentials(self): + pred = PredicateExpression() + count = FilterExpression() + # comparison + self.assertEqual(Has(pred, count), Has(pred, count)) + self.assertEqual(hash(Has(pred, count)), hash(Has(pred, count))) + # comparison respects type + self.assertNotEqual(Has(pred, count), FilterExpression()) + self.assertNotEqual(hash(Has(pred, count)), hash(FilterExpression())) + # comparison respects predicate + self.assertNotEqual(Has(pred, count), Has(Predicate(ns.bse.filesize), count)) + self.assertNotEqual(hash(Has(pred, count)), hash(Has(Predicate(ns.bse.filesize), count))) + # comparison respects count + self.assertNotEqual(Has(pred, count), Has(pred, LessThan(5))) + self.assertNotEqual(hash(Has(pred, count)), hash(Has(pred, LessThan(5)))) + # string conversion + self.assertEqual(str(Has(Predicate(ns.bse.filesize), LessThan(5))), + f'Has(Predicate({ns.bse.filesize}, False), LessThan(5.0, True))') + self.assertEqual(repr(Has(Predicate(ns.bse.filesize), LessThan(5))), + f'Has(Predicate({ns.bse.filesize}, False), LessThan(5.0, True))') + + def test_members(self): + pred = PredicateExpression() + count = FilterExpression() + # member returns expression + # predicate must be an URI or a PredicateExpression + self.assertEqual(Has(ns.bse.filesize, count).predicate, Predicate(ns.bse.filesize)) + self.assertEqual(Has(Predicate(ns.bse.filesize), count).predicate, Predicate(ns.bse.filesize)) + self.assertRaises(TypeError, Has, 1234, FilterExpression()) + self.assertRaises(TypeError, Has, FilterExpression(), FilterExpression()) + # member returns count + # count must be None, an integer, or a FilterExpression + self.assertEqual(Has(pred).count, GreaterThan(1, False)) + self.assertEqual(Has(pred, LessThan(5)).count, LessThan(5)) + self.assertEqual(Has(pred, 5).count, Equals(5)) + self.assertRaises(TypeError, Has, pred, 'hello') + self.assertRaises(TypeError, Has, pred, Predicate(ns.bse.filesize)) + + + +class TestValue(unittest.TestCase): + def test_essentials(self): + # comparison respects type + self.assertNotEqual(_Value('hello'), Equals('hello')) + self.assertNotEqual(Equals('hello'), Is('hello')) + self.assertNotEqual(Is('hello'), Substring('hello')) + self.assertNotEqual(Substring('hello'), StartsWith('hello')) + self.assertNotEqual(StartsWith('hello'), EndsWith('hello')) + self.assertNotEqual(EndsWith('hello'), _Value('hello')) + self.assertNotEqual(hash(_Value('hello')), hash(Equals('hello'))) + self.assertNotEqual(hash(Equals('hello')), hash(Is('hello'))) + self.assertNotEqual(hash(Is('hello')), hash(Substring('hello'))) + self.assertNotEqual(hash(Substring('hello')), hash(StartsWith('hello'))) + self.assertNotEqual(hash(StartsWith('hello')), hash(EndsWith('hello'))) + self.assertNotEqual(hash(EndsWith('hello')), hash(_Value('hello'))) + + for cls in (_Value, Is, Equals, Substring, StartsWith, EndsWith): + # comparison + self.assertEqual(cls('hello'), cls('hello')) + self.assertEqual(hash(cls('hello')), hash(cls('hello'))) + # comparison respects value + self.assertNotEqual(cls('hello'), cls('world')) + self.assertNotEqual(hash(cls('hello')), hash(cls('world'))) + + # string conversion + self.assertEqual(str(_Value('hello')), '_Value(hello)') + self.assertEqual(repr(_Value('hello')), '_Value(hello)') + self.assertEqual(str(Is('hello')), 'Is(hello)') + self.assertEqual(repr(Is('hello')), 'Is(hello)') + self.assertEqual(str(Equals('hello')), 'Equals(hello)') + self.assertEqual(repr(Equals('hello')), 'Equals(hello)') + self.assertEqual(str(Substring('hello')), 'Substring(hello)') + self.assertEqual(repr(Substring('hello')), 'Substring(hello)') + self.assertEqual(str(StartsWith('hello')), 'StartsWith(hello)') + self.assertEqual(repr(StartsWith('hello')), 'StartsWith(hello)') + self.assertEqual(str(EndsWith('hello')), 'EndsWith(hello)') + self.assertEqual(repr(EndsWith('hello')), 'EndsWith(hello)') + + def test_value(self): + class Foo(): pass + for cls in (_Value, Is, Equals, Substring, StartsWith, EndsWith): + # value can be anything + # value returns member + f = Foo() + self.assertEqual(cls('hello').value, 'hello') + self.assertEqual(cls(1234).value, 1234) + self.assertEqual(cls(f).value, f) + + +class TestBounded(unittest.TestCase): + def test_essentials(self): + # comparison respects type + self.assertNotEqual(_Bounded(1234), LessThan(1234)) + self.assertNotEqual(LessThan(1234), GreaterThan(1234)) + self.assertNotEqual(GreaterThan(1234), _Bounded(1234)) + self.assertNotEqual(hash(_Bounded(1234)), hash(LessThan(1234))) + self.assertNotEqual(hash(LessThan(1234)), hash(GreaterThan(1234))) + self.assertNotEqual(hash(GreaterThan(1234)), hash(_Bounded(1234))) + + for cls in (_Bounded, LessThan, GreaterThan): + # comparison + self.assertEqual(cls(1234), cls(1234)) + self.assertEqual(hash(cls(1234)), hash(cls(1234))) + # comparison respects threshold + self.assertNotEqual(cls(1234), cls(4321)) + self.assertNotEqual(hash(cls(1234)), hash(cls(4321))) + # comparison respects strict + self.assertNotEqual(cls(1234, True), cls(1234, False)) + self.assertNotEqual(hash(cls(1234, True)), hash(cls(1234, False))) + + # string conversion + self.assertEqual(str(_Bounded(1234, False)), '_Bounded(1234.0, False)') + self.assertEqual(repr(_Bounded(1234, False)), '_Bounded(1234.0, False)') + self.assertEqual(str(LessThan(1234, False)), 'LessThan(1234.0, False)') + self.assertEqual(repr(LessThan(1234, False)), 'LessThan(1234.0, False)') + self.assertEqual(str(GreaterThan(1234, False)), 'GreaterThan(1234.0, False)') + self.assertEqual(repr(GreaterThan(1234, False)), 'GreaterThan(1234.0, False)') + + def test_members(self): + class Foo(): pass + for cls in (_Bounded, LessThan, GreaterThan): + # threshold becomes float + self.assertEqual(cls(1.234).threshold, 1.234) + self.assertEqual(cls(1234).threshold, 1234.0) + self.assertEqual(cls('1234').threshold, 1234) + self.assertRaises(TypeError, cls, Foo()) + # strict becomes bool + self.assertEqual(cls(1234, True).strict, True) + self.assertEqual(cls(1234, False).strict, False) + self.assertEqual(cls(1234, Foo()).strict, True) + + +class TestPredicate(unittest.TestCase): + def test_essentials(self): + # comparison + self.assertEqual(Predicate(ns.bse.filesize), Predicate(ns.bse.filesize)) + self.assertEqual(hash(Predicate(ns.bse.filesize)), hash(Predicate(ns.bse.filesize))) + # comparison respects type + self.assertNotEqual(Predicate(ns.bse.filesize), PredicateExpression()) + self.assertNotEqual(hash(Predicate(ns.bse.filesize)), hash(PredicateExpression())) + # comparison respects predicate + self.assertNotEqual(Predicate(ns.bse.filesize), Predicate(ns.bse.filename)) + self.assertNotEqual(hash(Predicate(ns.bse.filesize)), hash(Predicate(ns.bse.filename))) + # comparison respects reverse + self.assertNotEqual(Predicate(ns.bse.filesize, True), Predicate(ns.bse.filesize, False)) + self.assertNotEqual(hash(Predicate(ns.bse.filesize, True)), hash(Predicate(ns.bse.filesize, False))) + # string conversion + self.assertEqual(str(Predicate(ns.bse.filesize)), f'Predicate({ns.bse.filesize}, False)') + self.assertEqual(str(Predicate(ns.bse.filesize, True)), + f'Predicate({ns.bse.filesize}, True)') + self.assertEqual(repr(Predicate(ns.bse.filesize)), f'Predicate({ns.bse.filesize}, False)') + self.assertEqual(repr(Predicate(ns.bse.filesize, True)), + f'Predicate({ns.bse.filesize}, True)') + + def test_members(self): + # member returns predicate + # predicate must be an URI + self.assertEqual(Predicate(ns.bse.filesize).predicate, ns.bse.filesize) + self.assertEqual(Predicate(URI('hello world')).predicate, URI('hello world')) + self.assertRaises(TypeError, Predicate, 1234) + self.assertRaises(TypeError, Predicate, FilterExpression()) + self.assertRaises(TypeError, Predicate, FilterExpression()) + # reverse becomes a boolean + self.assertEqual(Predicate(ns.bse.filesize, True).reverse, True) + self.assertEqual(Predicate(ns.bse.filesize, False).reverse, False) + self.assertEqual(Predicate(ns.bse.filesize, 'abc').reverse, True) + + +class TestOneOf(unittest.TestCase): + def test_essentials(self): + expr = {Predicate(ns.bse.filename), Predicate(ns.bse.filesize)} + # comparison + self.assertEqual(OneOf(expr), OneOf(expr)) + self.assertEqual(hash(OneOf(expr)), hash(OneOf(expr))) + # comparison respects type + self.assertNotEqual(OneOf(expr), PredicateExpression()) + self.assertNotEqual(hash(OneOf(expr)), hash(PredicateExpression())) + # comparison respects expression + self.assertNotEqual(OneOf(expr), OneOf(Predicate(ns.bse.filename))) + self.assertNotEqual(hash(OneOf(expr)), hash(OneOf(Predicate(ns.bse.filename)))) + # string conversion + self.assertEqual(str(OneOf(Predicate(ns.bse.filesize))), + f'OneOf({{Predicate({ns.bse.filesize}, False)}})') + self.assertEqual(repr(OneOf(Predicate(ns.bse.filesize))), + f'OneOf({{Predicate({ns.bse.filesize}, False)}})') + + def test_expression(self): + class Foo(): pass + # can pass expressions as arguments + self.assertSetEqual(OneOf(Predicate(ns.bse.filesize), Predicate(ns.bse.filename)).expr, + {Predicate(ns.bse.filesize), Predicate(ns.bse.filename)}) + # can pass one expressions as argument + self.assertSetEqual(OneOf(Predicate(ns.bse.filesize)).expr, + {Predicate(ns.bse.filesize)}) + # can pass expressions as iterator + self.assertSetEqual(OneOf(iter((Predicate(ns.bse.filesize), Predicate(ns.bse.filename)))).expr, + {Predicate(ns.bse.filesize), Predicate(ns.bse.filename)}) + # can pass expressions as generator + def gen(): + yield Predicate(ns.bse.filesize) + yield Predicate(ns.bse.filename) + self.assertSetEqual(OneOf(gen()).expr, + {Predicate(ns.bse.filesize), Predicate(ns.bse.filename)}) + # can pass expressions as list-like + self.assertSetEqual(OneOf((Predicate(ns.bse.filesize), Predicate(ns.bse.filename))).expr, + {Predicate(ns.bse.filesize), Predicate(ns.bse.filename)}) + # can pass one expression as list-like + self.assertSetEqual(OneOf([Predicate(ns.bse.filesize)]).expr, + {Predicate(ns.bse.filesize)}) + # must pass expressions + self.assertRaises(TypeError, OneOf, Foo(), Foo()) + self.assertRaises(TypeError, OneOf, [Foo(), Foo()]) + # must pass at least one expression + self.assertRaises(AttributeError, OneOf) + + # iter + self.assertSetEqual(set(iter(OneOf(Predicate(ns.bse.filesize), Predicate(ns.bse.filename)))), + {Predicate(ns.bse.filesize), Predicate(ns.bse.filename)}) + # contains + self.assertIn(Predicate(ns.bse.filesize), + OneOf(Predicate(ns.bse.filesize), Predicate(ns.bse.filename))) + self.assertNotIn(Predicate(ns.bse.tag), + OneOf(Predicate(ns.bse.filesize), Predicate(ns.bse.filename))) + # len + self.assertEqual(len(OneOf(Predicate(ns.bse.filesize), Predicate(ns.bse.filename))), 2) + self.assertEqual(len(OneOf(Predicate(ns.bse.filesize), Predicate(ns.bse.filename), Predicate(ns.bse.tag))), 3) + + + def testIsIn(self): + # can pass expressions as arguments + self.assertEqual(IsIn('http://example.com/entity#1234', 'http://example.com/entity#4321'), + Or(Is('http://example.com/entity#1234'), Is('http://example.com/entity#4321'))) + # can pass one expression as argument + self.assertEqual(IsIn('http://example.com/entity#1234'), + Or(Is('http://example.com/entity#1234'))) + # can pass expressions as iterator + self.assertEqual(IsIn(iter(('http://example.com/entity#1234', 'http://example.com/entity#4321'))), + Or(Is('http://example.com/entity#1234'), Is('http://example.com/entity#4321'))) + # can pass expressions as generator + def gen(): + yield 'http://example.com/entity#1234' + yield 'http://example.com/entity#4321' + self.assertEqual(IsIn(gen()), + Or(Is('http://example.com/entity#1234'), Is('http://example.com/entity#4321'))) + # can pass expressions as list-like + self.assertEqual(IsIn(['http://example.com/entity#1234', 'http://example.com/entity#4321']), + Or(Is('http://example.com/entity#1234'), Is('http://example.com/entity#4321'))) + # can pass one expression as list-like + self.assertEqual(IsIn(['http://example.com/entity#1234']), + Or(Is('http://example.com/entity#1234'))) + + + def testIsNotIn(self): + # can pass expressions as arguments + self.assertEqual(IsNotIn('http://example.com/entity#1234', 'http://example.com/entity#4321'), + Not(Or(Is('http://example.com/entity#1234'), Is('http://example.com/entity#4321')))) + # can pass one expression as argument + self.assertEqual(IsNotIn('http://example.com/entity#1234'), + Not(Or(Is('http://example.com/entity#1234')))) + # can pass expressions as iterator + self.assertEqual(IsNotIn(iter(('http://example.com/entity#1234', 'http://example.com/entity#4321'))), + Not(Or(Is('http://example.com/entity#1234'), Is('http://example.com/entity#4321')))) + # can pass expressions as generator + def gen(): + yield 'http://example.com/entity#1234' + yield 'http://example.com/entity#4321' + self.assertEqual(IsNotIn(gen()), + Not(Or(Is('http://example.com/entity#1234'), Is('http://example.com/entity#4321')))) + # can pass expressions as list-like + self.assertEqual(IsNotIn(['http://example.com/entity#1234', 'http://example.com/entity#4321']), + Not(Or(Is('http://example.com/entity#1234'), Is('http://example.com/entity#4321')))) + # can pass one expression as list-like + self.assertEqual(IsNotIn(['http://example.com/entity#1234']), + Not(Or(Is('http://example.com/entity#1234')))) + + + +## main ## + +if __name__ == '__main__': + unittest.main() + +## EOF ## -- cgit v1.2.3 From 6fd984e694b0a7b749ab947211d792f5b011ee6f Mon Sep 17 00:00:00 2001 From: Matthias Baumgartner Date: Thu, 12 Jan 2023 08:44:25 +0100 Subject: renamed get_child to child in schema.types._Type and _Vertex to Vertex in schema.types --- bsfs/schema/__init__.py | 2 +- bsfs/schema/schema.py | 2 +- bsfs/schema/serialize.py | 4 +- bsfs/schema/types.py | 28 +++--- test/graph/test_resolve.py | 2 +- test/query/test_validator.py | 22 ++--- test/schema/test_schema.py | 40 ++++---- test/schema/test_serialize.py | 126 +++++++++++++------------- test/schema/test_types.py | 108 +++++++++++----------- test/triple_store/sparql/test_parse_filter.py | 2 +- test/triple_store/sparql/test_sparql.py | 10 +- 11 files changed, 173 insertions(+), 173 deletions(-) diff --git a/bsfs/schema/__init__.py b/bsfs/schema/__init__.py index dc24313..5162a01 100644 --- a/bsfs/schema/__init__.py +++ b/bsfs/schema/__init__.py @@ -10,7 +10,7 @@ import typing # inner-module imports from .schema import Schema from .serialize import from_string, to_string -from .types import Literal, Node, Predicate, _Vertex # FIXME: _Vertex +from .types import Literal, Node, Predicate, Vertex, ROOT_FEATURE, ROOT_LITERAL, ROOT_NODE, ROOT_NUMBER, ROOT_PREDICATE, ROOT_VERTEX # exports __all__: typing.Sequence[str] = ( diff --git a/bsfs/schema/schema.py b/bsfs/schema/schema.py index 1c4c807..80cb58a 100644 --- a/bsfs/schema/schema.py +++ b/bsfs/schema/schema.py @@ -83,7 +83,7 @@ class Schema(): prange = {pred.range for pred in predicates} nodes |= {vert for vert in prange if isinstance(vert, types.Node)} literals |= {vert for vert in prange if isinstance(vert, types.Literal)} - # NOTE: ROOT_PREDICATE has a _Vertex as range which is neither in nodes nor literals + # NOTE: ROOT_PREDICATE has a Vertex as range which is neither in nodes nor literals # FIXME: with the ROOT_VERTEX missing, the schema is not complete anymore! # include parents in nodes and literals sets diff --git a/bsfs/schema/serialize.py b/bsfs/schema/serialize.py index 1222aa6..c1ac9a9 100644 --- a/bsfs/schema/serialize.py +++ b/bsfs/schema/serialize.py @@ -125,10 +125,10 @@ def from_string(schema_str: str) -> schema.Schema: # get distance distance = _fetch_value(uri, rdflib.URIRef(ns.bsfs.distance), URI) # return feature - return parent.get_child(URI(uri), domain=dom, range=rng, unique=unique, + return parent.child(URI(uri), domain=dom, range=rng, unique=unique, dtype=dtype, dimension=dimension, distance=distance, **annotations) # handle non-feature predicate - return parent.get_child(URI(uri), domain=dom, range=rng, unique=unique, **annotations) + return parent.child(URI(uri), domain=dom, range=rng, unique=unique, **annotations) predicates = _fetch_hierarchically(_build_predicate, types.ROOT_PREDICATE) return schema.Schema(predicates, nodes, literals) diff --git a/bsfs/schema/types.py b/bsfs/schema/types.py index e737263..4f49efe 100644 --- a/bsfs/schema/types.py +++ b/bsfs/schema/types.py @@ -114,7 +114,7 @@ class _Type(): yield curr curr = curr.parent - def get_child( + def child( self, uri: URI, **kwargs, @@ -201,21 +201,21 @@ class _Type(): return False -class _Vertex(_Type): +class Vertex(_Type): """Graph vertex types. Can be a Node or a Literal.""" - parent: typing.Optional['_Vertex'] - def __init__(self, uri: URI, parent: typing.Optional['_Vertex'], **kwargs): + parent: typing.Optional['Vertex'] + def __init__(self, uri: URI, parent: typing.Optional['Vertex'], **kwargs): super().__init__(uri, parent, **kwargs) -class Node(_Vertex): +class Node(Vertex): """Node type.""" parent: typing.Optional['Node'] def __init__(self, uri: URI, parent: typing.Optional['Node'], **kwargs): super().__init__(uri, parent, **kwargs) -class Literal(_Vertex): +class Literal(Vertex): """Literal type.""" parent: typing.Optional['Literal'] def __init__(self, uri: URI, parent: typing.Optional['Literal'] ,**kwargs): @@ -229,7 +229,7 @@ class Predicate(_Type): domain: Node # destination type. - range: _Vertex + range: Vertex # maximum cardinality of type. unique: bool @@ -241,7 +241,7 @@ class Predicate(_Type): parent: '_PredicateBase', # Predicate members domain: Node, - range: _Vertex, # pylint: disable=redefined-builtin + range: Vertex, # pylint: disable=redefined-builtin unique: bool, **kwargs, ): @@ -265,11 +265,11 @@ class Predicate(_Type): and self.range == other.range \ and self.unique == other.unique - def get_child( + def child( self, uri: URI, domain: typing.Optional[Node] = None, - range: typing.Optional[_Vertex] = None, # pylint: disable=redefined-builtin + range: typing.Optional[Vertex] = None, # pylint: disable=redefined-builtin unique: typing.Optional[bool] = None, **kwargs, ): @@ -287,7 +287,7 @@ class Predicate(_Type): raise errors.ConsistencyError(f'{range} must be a subclass of {self.range}') if unique is None: unique = self.unique - return super().get_child( + return super().child( uri=uri, domain=domain, range=range, @@ -337,7 +337,7 @@ class Feature(Predicate): and self.dtype == other.dtype \ and self.distance == other.distance - def get_child( + def child( self, uri: URI, domain: typing.Optional[Node] = None, @@ -355,7 +355,7 @@ class Feature(Predicate): dtype = self.dtype if distance is None: distance = self.distance - return super().get_child( + return super().child( uri=uri, domain=domain, range=range, @@ -368,7 +368,7 @@ class Feature(Predicate): # essential vertices -ROOT_VERTEX = _Vertex( +ROOT_VERTEX = Vertex( uri=ns.bsfs.Vertex, parent=None, ) diff --git a/test/graph/test_resolve.py b/test/graph/test_resolve.py index 5bc99e4..f515320 100644 --- a/test/graph/test_resolve.py +++ b/test/graph/test_resolve.py @@ -65,7 +65,7 @@ class TestFilter(unittest.TestCase): {URI('http://example.com/me/entity#1234'), URI('http://example.com/me/entity#4321')}) tags = graph.nodes(ns.bsfs.Tag, {URI('http://example.com/me/tag#1234'), URI('http://example.com/me/tag#4321')}) - invalid = nodes.Nodes(None, '', schema.node(ns.bsfs.Node).get_child(ns.bsfs.Invalid), + invalid = nodes.Nodes(None, '', schema.node(ns.bsfs.Node).child(ns.bsfs.Invalid), {'http://example.com/you/invalid#1234', 'http://example.com/you/invalid#4321'}) resolver = Filter(schema) diff --git a/test/query/test_validator.py b/test/query/test_validator.py index 4f8364a..bf3ceeb 100644 --- a/test/query/test_validator.py +++ b/test/query/test_validator.py @@ -69,8 +69,8 @@ class TestFilter(unittest.TestCase): self.assertRaises(TypeError, self.validate, '1234', None) self.assertRaises(TypeError, self.validate, self.schema.literal(ns.bsfs.URI), None) # root_type must exist in the schema - self.assertRaises(errors.ConsistencyError, self.validate, self.schema.node(ns.bsfs.Node).get_child(ns.bsfs.Image), None) - self.assertRaises(errors.ConsistencyError, self.validate, self.schema.node(ns.bsfs.Entity).get_child(ns.bsfs.Image), None) + self.assertRaises(errors.ConsistencyError, self.validate, self.schema.node(ns.bsfs.Node).child(ns.bsfs.Image), None) + self.assertRaises(errors.ConsistencyError, self.validate, self.schema.node(ns.bsfs.Entity).child(ns.bsfs.Image), None) # valid query returns true self.assertTrue(self.validate(self.schema.node(ns.bsfs.Entity), ast.filter.Any(ast.filter.OneOf(ns.bse.tag, ns.bse.buddy), @@ -130,7 +130,7 @@ class TestFilter(unittest.TestCase): # type must be a node self.assertRaises(errors.ConsistencyError, self.validate._branch, self.schema.literal(ns.bsfs.Literal), None) # type must be in the schema - self.assertRaises(errors.ConsistencyError, self.validate._branch, self.schema.node(ns.bsfs.Node).get_child(ns.bsfs.Invalid), None) + self.assertRaises(errors.ConsistencyError, self.validate._branch, self.schema.node(ns.bsfs.Node).child(ns.bsfs.Invalid), None) # predicate is verified self.assertRaises(errors.ConsistencyError, self.validate._branch, self.schema.node(ns.bsfs.Entity), ast.filter.Any(ns.bsfs.Invalid, ast.filter.Is('http://example.com/entity#1234'))) @@ -187,7 +187,7 @@ class TestFilter(unittest.TestCase): self.assertRaises(errors.ConsistencyError, self.validate._has, self.schema.literal(ns.bsfs.Literal), ast.filter.Has(ns.bse.tag)) # type must be in the schema - self.assertRaises(errors.ConsistencyError, self.validate._has, self.schema.node(ns.bsfs.Node).get_child(ns.bsfs.Invalid), + self.assertRaises(errors.ConsistencyError, self.validate._has, self.schema.node(ns.bsfs.Node).child(ns.bsfs.Invalid), ast.filter.Has(ns.bse.tag)) # has checks predicate self.assertRaises(errors.ConsistencyError, self.validate._has, self.schema.node(ns.bsfs.Entity), @@ -206,7 +206,7 @@ class TestFilter(unittest.TestCase): self.assertRaises(errors.ConsistencyError, self.validate._is, self.schema.literal(ns.bsfs.Literal), ast.filter.Is('http://example.com/foo')) # type must be in the schema - self.assertRaises(errors.ConsistencyError, self.validate._is, self.schema.node(ns.bsfs.Node).get_child(ns.bsfs.Invalid), + self.assertRaises(errors.ConsistencyError, self.validate._is, self.schema.node(ns.bsfs.Node).child(ns.bsfs.Invalid), ast.filter.Is('http://example.com/foo')) # is accepts correct expressions self.assertIsNone(self.validate._is(self.schema.node(ns.bsfs.Entity), ast.filter.Is('http://example.com/entity#1234'))) @@ -222,13 +222,13 @@ class TestFilter(unittest.TestCase): self.assertRaises(errors.ConsistencyError, self.validate._value, self.schema.node(ns.bsfs.Node), ast.filter.EndsWith('hello world')) # type must be in the schema - self.assertRaises(errors.ConsistencyError, self.validate._value, self.schema.literal(ns.bsfs.Literal).get_child(ns.bsfs.Invalid), + self.assertRaises(errors.ConsistencyError, self.validate._value, self.schema.literal(ns.bsfs.Literal).child(ns.bsfs.Invalid), ast.filter.Equals('hello world')) - self.assertRaises(errors.ConsistencyError, self.validate._value, self.schema.literal(ns.bsfs.Literal).get_child(ns.bsfs.Invalid), + self.assertRaises(errors.ConsistencyError, self.validate._value, self.schema.literal(ns.bsfs.Literal).child(ns.bsfs.Invalid), ast.filter.Substring('hello world')) - self.assertRaises(errors.ConsistencyError, self.validate._value, self.schema.literal(ns.bsfs.Literal).get_child(ns.bsfs.Invalid), + self.assertRaises(errors.ConsistencyError, self.validate._value, self.schema.literal(ns.bsfs.Literal).child(ns.bsfs.Invalid), ast.filter.StartsWith('hello world')) - self.assertRaises(errors.ConsistencyError, self.validate._value, self.schema.literal(ns.bsfs.Literal).get_child(ns.bsfs.Invalid), + self.assertRaises(errors.ConsistencyError, self.validate._value, self.schema.literal(ns.bsfs.Literal).child(ns.bsfs.Invalid), ast.filter.EndsWith('hello world')) # value accepts correct expressions self.assertIsNone(self.validate._value(self.schema.literal(ns.xsd.string), ast.filter.Equals('hello world'))) @@ -243,9 +243,9 @@ class TestFilter(unittest.TestCase): self.assertRaises(errors.ConsistencyError, self.validate._bounded, self.schema.node(ns.bsfs.Node), ast.filter.LessThan(0)) # type must be in the schema - self.assertRaises(errors.ConsistencyError, self.validate._bounded, self.schema.literal(ns.bsfs.Literal).get_child(ns.bsfs.Invalid), + self.assertRaises(errors.ConsistencyError, self.validate._bounded, self.schema.literal(ns.bsfs.Literal).child(ns.bsfs.Invalid), ast.filter.GreaterThan(0)) - self.assertRaises(errors.ConsistencyError, self.validate._bounded, self.schema.literal(ns.bsfs.Literal).get_child(ns.bsfs.Invalid), + self.assertRaises(errors.ConsistencyError, self.validate._bounded, self.schema.literal(ns.bsfs.Literal).child(ns.bsfs.Invalid), ast.filter.LessThan(0)) # bounded accepts correct expressions self.assertIsNone(self.validate._bounded(self.schema.literal(ns.xsd.integer), ast.filter.LessThan(0))) diff --git a/test/schema/test_schema.py b/test/schema/test_schema.py index 1b45db0..ca21f87 100644 --- a/test/schema/test_schema.py +++ b/test/schema/test_schema.py @@ -73,9 +73,9 @@ class TestSchema(unittest.TestCase): # predicates self.p_root = types.ROOT_PREDICATE self.f_root = types.ROOT_FEATURE - self.p_tag = self.p_root.get_child(ns.bse.tag, self.n_ent, self.n_tag, False) - self.p_group = self.p_tag.get_child(ns.bse.group, self.n_img, self.n_tag, False) - self.p_comment = self.p_root.get_child(ns.bse.comment, self.n_root, self.l_string, True) + self.p_tag = self.p_root.child(ns.bse.tag, self.n_ent, self.n_tag, False) + self.p_group = self.p_tag.child(ns.bse.group, self.n_img, self.n_tag, False) + self.p_comment = self.p_root.child(ns.bse.comment, self.n_root, self.l_string, True) self.predicates = [self.p_root, self.f_root, self.p_tag, self.p_group, self.p_comment] def test_construction(self): @@ -217,16 +217,16 @@ class TestSchema(unittest.TestCase): self.assertNotEqual(hash(schema), hash(Schema([self.p_group, self.p_tag, self.p_root], self.nodes, self.literals))) self.assertNotEqual(schema, - Schema(self.predicates + [self.p_root.get_child(ns.bse.filesize, self.n_ent, self.l_integer)], self.nodes, self.literals)) + Schema(self.predicates + [self.p_root.child(ns.bse.filesize, self.n_ent, self.l_integer)], self.nodes, self.literals)) self.assertNotEqual(hash(schema), - hash(Schema(self.predicates + [self.p_root.get_child(ns.bse.filesize, self.n_ent, self.l_integer)], self.nodes, self.literals))) + hash(Schema(self.predicates + [self.p_root.child(ns.bse.filesize, self.n_ent, self.l_integer)], self.nodes, self.literals))) def test_order(self): # setup class Foo(): pass - p_foo = self.p_root.get_child(ns.bse.foo, self.n_ent, self.l_string, True) - p_sub = p_foo.get_child(ns.bse.sub, self.n_ent, self.l_string, True) - p_bar = self.p_root.get_child(ns.bse.bar, self.n_ent, self.l_string, True) + p_foo = self.p_root.child(ns.bse.foo, self.n_ent, self.l_string, True) + p_sub = p_foo.child(ns.bse.sub, self.n_ent, self.l_string, True) + p_bar = self.p_root.child(ns.bse.bar, self.n_ent, self.l_string, True) # can only compare schema to other schema # < @@ -305,44 +305,44 @@ class TestSchema(unittest.TestCase): # inconsistent schema cannot be a subset self.assertFalse(operator.le(Schema({p_foo}), Schema({ - self.p_root.get_child(ns.bse.foo, self.n_ent, self.l_integer, True)}))) # inconsistent w.r.t. literal + self.p_root.child(ns.bse.foo, self.n_ent, self.l_integer, True)}))) # inconsistent w.r.t. literal self.assertFalse(operator.le(Schema({p_foo}), Schema({ - self.p_root.get_child(ns.bse.foo, self.n_img, self.l_string, True)}))) # inconsistent w.r.t. node + self.p_root.child(ns.bse.foo, self.n_img, self.l_string, True)}))) # inconsistent w.r.t. node self.assertFalse(operator.le(Schema({p_foo}), Schema({ - self.p_root.get_child(ns.bse.foo, self.n_ent, self.l_string, False)}))) # inconsistent w.r.t. unique + self.p_root.child(ns.bse.foo, self.n_ent, self.l_string, False)}))) # inconsistent w.r.t. unique self.assertFalse(operator.le(Schema({}, {self.n_img}), Schema({}, { types.Node(ns.bsfs.Image, types.Node(ns.bsfs.Node, None))}))) self.assertFalse(operator.le(Schema({}, {}, {self.l_integer}), Schema({}, {}, { types.Literal(ns.xsd.integer, types.Literal(ns.xsd.number, types.Literal(ns.bsfs.Literal, None)))}))) # inconsistent schema cannot be a true subset self.assertFalse(operator.lt(Schema({p_foo}), Schema({ - self.p_root.get_child(ns.bse.foo, self.n_ent, self.l_integer, True)}))) # inconsistent w.r.t. literal + self.p_root.child(ns.bse.foo, self.n_ent, self.l_integer, True)}))) # inconsistent w.r.t. literal self.assertFalse(operator.lt(Schema({p_foo}), Schema({ - self.p_root.get_child(ns.bse.foo, self.n_img, self.l_string, True)}))) # inconsistent w.r.t. node + self.p_root.child(ns.bse.foo, self.n_img, self.l_string, True)}))) # inconsistent w.r.t. node self.assertFalse(operator.lt(Schema({p_foo}), Schema({ - self.p_root.get_child(ns.bse.foo, self.n_ent, self.l_string, False)}))) # inconsistent w.r.t. unique + self.p_root.child(ns.bse.foo, self.n_ent, self.l_string, False)}))) # inconsistent w.r.t. unique self.assertFalse(operator.lt(Schema({}, {self.n_img}), Schema({}, { types.Node(ns.bsfs.Image, types.Node(ns.bsfs.Node, None))}))) self.assertFalse(operator.lt(Schema({}, {}, {self.l_integer}), Schema({}, {}, { types.Literal(ns.xsd.integer, types.Literal(ns.xsd.number, types.Literal(ns.bsfs.Literal, None)))}))) # inconsistent schema cannot be a superset self.assertFalse(operator.ge(Schema({p_foo}), Schema({ - self.p_root.get_child(ns.bse.foo, self.n_ent, self.l_integer, True)}))) # inconsistent w.r.t. literal + self.p_root.child(ns.bse.foo, self.n_ent, self.l_integer, True)}))) # inconsistent w.r.t. literal self.assertFalse(operator.ge(Schema({p_foo}), Schema({ - self.p_root.get_child(ns.bse.foo, self.n_img, self.l_string, True)}))) # inconsistent w.r.t. node + self.p_root.child(ns.bse.foo, self.n_img, self.l_string, True)}))) # inconsistent w.r.t. node self.assertFalse(operator.ge(Schema({p_foo}), Schema({ - self.p_root.get_child(ns.bse.foo, self.n_ent, self.l_string, False)}))) # inconsistent w.r.t. unique + self.p_root.child(ns.bse.foo, self.n_ent, self.l_string, False)}))) # inconsistent w.r.t. unique self.assertFalse(operator.ge(Schema({}, {self.n_img}), Schema({}, { types.Node(ns.bsfs.Image, types.Node(ns.bsfs.Node, None))}))) self.assertFalse(operator.ge(Schema({}, {}, {self.l_integer}), Schema({}, {}, { types.Literal(ns.xsd.integer, types.Literal(ns.xsd.number, types.Literal(ns.bsfs.Literal, None)))}))) # inconsistent schema cannot be a true superset self.assertFalse(operator.gt(Schema({p_foo}), Schema({ - self.p_root.get_child(ns.bse.foo, self.n_ent, self.l_integer, True)}))) # inconsistent w.r.t. literal + self.p_root.child(ns.bse.foo, self.n_ent, self.l_integer, True)}))) # inconsistent w.r.t. literal self.assertFalse(operator.gt(Schema({p_foo}), Schema({ - self.p_root.get_child(ns.bse.foo, self.n_img, self.l_string, True)}))) # inconsistent w.r.t. node + self.p_root.child(ns.bse.foo, self.n_img, self.l_string, True)}))) # inconsistent w.r.t. node self.assertFalse(operator.gt(Schema({p_foo}), Schema({ - self.p_root.get_child(ns.bse.foo, self.n_ent, self.l_string, False)}))) # inconsistent w.r.t. unique + self.p_root.child(ns.bse.foo, self.n_ent, self.l_string, False)}))) # inconsistent w.r.t. unique self.assertFalse(operator.gt(Schema({}, {self.n_img}), Schema({}, { types.Node(ns.bsfs.Image, types.Node(ns.bsfs.Node, None))}))) self.assertFalse(operator.gt(Schema({}, {}, {self.l_integer}), Schema({}, {}, { diff --git a/test/schema/test_serialize.py b/test/schema/test_serialize.py index 7392cc0..b9d8599 100644 --- a/test/schema/test_serialize.py +++ b/test/schema/test_serialize.py @@ -66,7 +66,7 @@ class TestFromString(unittest.TestCase): ''') # additional nodes can be defined - n_unused = types.ROOT_NODE.get_child(ns.bsfs.unused) + n_unused = types.ROOT_NODE.child(ns.bsfs.unused) self.assertEqual(Schema({}, {n_unused}), from_string(''' prefix rdfs: prefix xsd: @@ -77,10 +77,10 @@ class TestFromString(unittest.TestCase): ''')) # a node can have multiple children - n_ent = types.ROOT_NODE.get_child(ns.bsfs.Entity) - n_tag = types.ROOT_NODE.get_child(ns.bsfs.Tag) - n_doc = n_ent.get_child(ns.bsfs.Document) - n_image = n_ent.get_child(ns.bsfs.Image) + n_ent = types.ROOT_NODE.child(ns.bsfs.Entity) + n_tag = types.ROOT_NODE.child(ns.bsfs.Tag) + n_doc = n_ent.child(ns.bsfs.Document) + n_image = n_ent.child(ns.bsfs.Image) self.assertEqual(Schema({}, {n_ent, n_tag, n_doc, n_image}), from_string(''' prefix rdfs: prefix xsd: @@ -97,9 +97,9 @@ class TestFromString(unittest.TestCase): ''')) # additional nodes can be defined and used - n_ent = types.ROOT_NODE.get_child(ns.bsfs.Entity) - l_string = types.ROOT_LITERAL.get_child(ns.xsd.string) - p_filename = types.ROOT_PREDICATE.get_child(ns.bse.filename, + n_ent = types.ROOT_NODE.child(ns.bsfs.Entity) + l_string = types.ROOT_LITERAL.child(ns.xsd.string) + p_filename = types.ROOT_PREDICATE.child(ns.bse.filename, n_ent, l_string, False) self.assertEqual(Schema({p_filename}), from_string(''' prefix rdfs: @@ -168,7 +168,7 @@ class TestFromString(unittest.TestCase): ''') # additional literals can be defined - l_unused = types.ROOT_LITERAL.get_child(ns.xsd.unused) + l_unused = types.ROOT_LITERAL.child(ns.xsd.unused) self.assertEqual(Schema({}, {}, {l_unused}), from_string(''' prefix rdfs: prefix xsd: @@ -179,10 +179,10 @@ class TestFromString(unittest.TestCase): ''')) # a literal can have multiple children - l_string = types.ROOT_LITERAL.get_child(ns.xsd.string) - l_integer = types.ROOT_LITERAL.get_child(ns.xsd.integer) - l_unsigned = l_integer.get_child(ns.xsd.unsigned) - l_signed = l_integer.get_child(ns.xsd.signed) + l_string = types.ROOT_LITERAL.child(ns.xsd.string) + l_integer = types.ROOT_LITERAL.child(ns.xsd.integer) + l_unsigned = l_integer.child(ns.xsd.unsigned) + l_signed = l_integer.child(ns.xsd.signed) self.assertEqual(Schema({}, {}, {l_string, l_integer, l_unsigned, l_signed}), from_string(''' prefix rdfs: prefix xsd: @@ -199,9 +199,9 @@ class TestFromString(unittest.TestCase): ''')) # additional literals can be defined and used - n_ent = types.ROOT_NODE.get_child(ns.bsfs.Entity) - l_string = types.ROOT_LITERAL.get_child(ns.xsd.string) - p_filename = types.ROOT_PREDICATE.get_child(ns.bse.filename, + n_ent = types.ROOT_NODE.child(ns.bsfs.Entity) + l_string = types.ROOT_LITERAL.child(ns.xsd.string) + p_filename = types.ROOT_PREDICATE.child(ns.bse.filename, n_ent, l_string, False) self.assertEqual(Schema({p_filename}), from_string(''' prefix rdfs: @@ -317,9 +317,9 @@ class TestFromString(unittest.TestCase): ''') # additional predicates can be defined - n_ent = types.ROOT_NODE.get_child(ns.bsfs.Entity) - l_string = types.ROOT_LITERAL.get_child(ns.xsd.string) - p_comment = types.ROOT_PREDICATE.get_child(ns.bse.comment, domain=n_ent, range=l_string, unique=False) + n_ent = types.ROOT_NODE.child(ns.bsfs.Entity) + l_string = types.ROOT_LITERAL.child(ns.xsd.string) + p_comment = types.ROOT_PREDICATE.child(ns.bse.comment, domain=n_ent, range=l_string, unique=False) self.assertEqual(Schema({p_comment}), from_string(''' prefix rdfs: prefix xsd: @@ -336,10 +336,10 @@ class TestFromString(unittest.TestCase): ''')) # predicates inherit properties from parents - n_ent = types.ROOT_NODE.get_child(ns.bsfs.Entity) - l_string = types.ROOT_LITERAL.get_child(ns.xsd.string) - p_annotation = types.ROOT_PREDICATE.get_child(ns.bsfs.Annotation, domain=n_ent, range=l_string) - p_comment = p_annotation.get_child(ns.bse.comment, unique=True) + n_ent = types.ROOT_NODE.child(ns.bsfs.Entity) + l_string = types.ROOT_LITERAL.child(ns.xsd.string) + p_annotation = types.ROOT_PREDICATE.child(ns.bsfs.Annotation, domain=n_ent, range=l_string) + p_comment = p_annotation.child(ns.bse.comment, unique=True) self.assertEqual(Schema({p_comment}), from_string(''' prefix rdfs: prefix xsd: @@ -358,10 +358,10 @@ class TestFromString(unittest.TestCase): ''')) # we can define partial predicates (w/o specifying a usable range) - n_ent = types.ROOT_NODE.get_child(ns.bsfs.Entity) - l_string = types.ROOT_LITERAL.get_child(ns.xsd.string) - p_annotation = types.ROOT_PREDICATE.get_child(ns.bsfs.Annotation, domain=n_ent) - p_comment = p_annotation.get_child(ns.bse.comment, range=l_string, unique=False) + n_ent = types.ROOT_NODE.child(ns.bsfs.Entity) + l_string = types.ROOT_LITERAL.child(ns.xsd.string) + p_annotation = types.ROOT_PREDICATE.child(ns.bsfs.Annotation, domain=n_ent) + p_comment = p_annotation.child(ns.bse.comment, range=l_string, unique=False) self.assertEqual(Schema({p_comment}), from_string(''' prefix rdfs: prefix xsd: @@ -381,8 +381,8 @@ class TestFromString(unittest.TestCase): # predicate definition can be split across multiple statements. # statements can be repeated - n_ent = types.ROOT_NODE.get_child(ns.bsfs.Entity) - p_foo = types.ROOT_PREDICATE.get_child(ns.bse.foo, domain=n_ent, range=types.ROOT_NODE, unique=True) + n_ent = types.ROOT_NODE.child(ns.bsfs.Entity) + p_foo = types.ROOT_PREDICATE.child(ns.bse.foo, domain=n_ent, range=types.ROOT_NODE, unique=True) self.assertEqual(Schema({p_foo}), from_string(''' prefix rdfs: prefix xsd: @@ -400,11 +400,11 @@ class TestFromString(unittest.TestCase): ''')) # domain must be a subtype of parent's domain - n_ent = types.ROOT_NODE.get_child(ns.bsfs.Entity) - n_image = n_ent.get_child(ns.bsfs.Image) - p_foo = types.ROOT_PREDICATE.get_child(ns.bse.foo, domain=types.ROOT_NODE) - p_bar = p_foo.get_child(ns.bse.bar, domain=n_ent) - p_foobar = p_bar.get_child(ns.bse.foobar, domain=n_image) + n_ent = types.ROOT_NODE.child(ns.bsfs.Entity) + n_image = n_ent.child(ns.bsfs.Image) + p_foo = types.ROOT_PREDICATE.child(ns.bse.foo, domain=types.ROOT_NODE) + p_bar = p_foo.child(ns.bse.bar, domain=n_ent) + p_foobar = p_bar.child(ns.bse.foobar, domain=n_image) self.assertEqual(Schema({p_foobar}), from_string(''' prefix rdfs: prefix xsd: @@ -439,11 +439,11 @@ class TestFromString(unittest.TestCase): ''') # range must be a subtype of parent's range - n_ent = types.ROOT_NODE.get_child(ns.bsfs.Entity) - n_image = n_ent.get_child(ns.bsfs.Image) - p_foo = types.ROOT_PREDICATE.get_child(ns.bse.foo, range=types.ROOT_NODE) - p_bar = p_foo.get_child(ns.bse.bar, range=n_ent) - p_foobar = p_bar.get_child(ns.bse.foobar, range=n_image) + n_ent = types.ROOT_NODE.child(ns.bsfs.Entity) + n_image = n_ent.child(ns.bsfs.Image) + p_foo = types.ROOT_PREDICATE.child(ns.bse.foo, range=types.ROOT_NODE) + p_bar = p_foo.child(ns.bse.bar, range=n_ent) + p_foobar = p_bar.child(ns.bse.foobar, range=n_image) self.assertEqual(Schema({p_foobar}), from_string(''' prefix rdfs: prefix xsd: @@ -658,9 +658,9 @@ class TestFromString(unittest.TestCase): ''') # additional predicates can be defined - n_ent = types.ROOT_NODE.get_child(ns.bsfs.Entity) - l_array = types.ROOT_LITERAL.get_child(ns.bsfs.array) - p_comment = types.ROOT_FEATURE.get_child(ns.bse.colors, domain=n_ent, range=l_array, unique=False) + n_ent = types.ROOT_NODE.child(ns.bsfs.Entity) + l_array = types.ROOT_LITERAL.child(ns.bsfs.array) + p_comment = types.ROOT_FEATURE.child(ns.bse.colors, domain=n_ent, range=l_array, unique=False) self.assertEqual(Schema({p_comment}), from_string(''' prefix rdfs: prefix xsd: @@ -678,12 +678,12 @@ class TestFromString(unittest.TestCase): ''')) # features inherit properties from parents - n_ent = types.ROOT_NODE.get_child(ns.bsfs.Entity) - l_array = types.ROOT_LITERAL.get_child(ns.bsfs.array) - l_string = types.ROOT_LITERAL.get_child(ns.xsd.string) - p_annotation = types.ROOT_FEATURE.get_child(ns.bsfs.Annotation, domain=n_ent, range=l_array, + n_ent = types.ROOT_NODE.child(ns.bsfs.Entity) + l_array = types.ROOT_LITERAL.child(ns.bsfs.array) + l_string = types.ROOT_LITERAL.child(ns.xsd.string) + p_annotation = types.ROOT_FEATURE.child(ns.bsfs.Annotation, domain=n_ent, range=l_array, dimension=1234, dtype=ns.xsd.string) - p_comment = p_annotation.get_child(ns.bse.colors, unique=True) + p_comment = p_annotation.child(ns.bse.colors, unique=True) self.assertEqual(Schema({p_comment}), from_string(''' prefix rdfs: prefix xsd: @@ -706,8 +706,8 @@ class TestFromString(unittest.TestCase): # feature definition can be split across multiple statements. # statements can be repeated - n_ent = types.ROOT_NODE.get_child(ns.bsfs.Entity) - p_foo = types.ROOT_FEATURE.get_child(ns.bse.foo, domain=n_ent, unique=True, + n_ent = types.ROOT_NODE.child(ns.bsfs.Entity) + p_foo = types.ROOT_FEATURE.child(ns.bse.foo, domain=n_ent, unique=True, dimension=1234, dtype=ns.bsfs.f32) self.assertEqual(Schema({p_foo}), from_string(''' prefix rdfs: @@ -887,24 +887,24 @@ class TestFromString(unittest.TestCase): def test_integration(self): # nodes - n_ent = types.ROOT_NODE.get_child(ns.bsfs.Entity) - n_tag = types.ROOT_NODE.get_child(ns.bsfs.Tag) - n_image = n_ent.get_child(ns.bsfs.Image) + n_ent = types.ROOT_NODE.child(ns.bsfs.Entity) + n_tag = types.ROOT_NODE.child(ns.bsfs.Tag) + n_image = n_ent.child(ns.bsfs.Image) # literals - l_string = types.ROOT_LITERAL.get_child(ns.xsd.string) - l_array = types.ROOT_LITERAL.get_child(ns.bsfs.array) - l_integer = types.ROOT_LITERAL.get_child(ns.xsd.integer) - l_boolean = types.ROOT_LITERAL.get_child(ns.xsd.boolean) + l_string = types.ROOT_LITERAL.child(ns.xsd.string) + l_array = types.ROOT_LITERAL.child(ns.bsfs.array) + l_integer = types.ROOT_LITERAL.child(ns.xsd.integer) + l_boolean = types.ROOT_LITERAL.child(ns.xsd.boolean) # predicates - p_annotation = types.ROOT_PREDICATE.get_child(ns.bsfs.Annotation) - p_tag = types.ROOT_PREDICATE.get_child(ns.bse.tag, domain=n_ent, range=n_tag) - p_group = p_tag.get_child(ns.bse.group, domain=n_image, unique=True) - p_comment = p_annotation.get_child(ns.bse.comment, range=l_string) + p_annotation = types.ROOT_PREDICATE.child(ns.bsfs.Annotation) + p_tag = types.ROOT_PREDICATE.child(ns.bse.tag, domain=n_ent, range=n_tag) + p_group = p_tag.child(ns.bse.group, domain=n_image, unique=True) + p_comment = p_annotation.child(ns.bse.comment, range=l_string) # features - f_colors = types.ROOT_FEATURE.get_child(URI('http://bsfs.ai/schema/Feature/colors_spatial'), + f_colors = types.ROOT_FEATURE.child(URI('http://bsfs.ai/schema/Feature/colors_spatial'), domain=n_ent, range=l_array, unique=True, dtype=ns.bsfs.f16, distance=ns.bsfs.euclidean) - f_colors1234 = f_colors.get_child(URI('http://bsfs.ai/schema/Feature/colors_spatial#1234'), dimension=1024) - f_colors4321 = f_colors.get_child(URI('http://bsfs.ai/schema/Feature/colors_spatial#4321'), dimension=2048) + f_colors1234 = f_colors.child(URI('http://bsfs.ai/schema/Feature/colors_spatial#1234'), dimension=1024) + f_colors4321 = f_colors.child(URI('http://bsfs.ai/schema/Feature/colors_spatial#4321'), dimension=2048) # schema ref = Schema( {p_annotation, p_tag, p_group, p_comment, f_colors, f_colors1234, f_colors4321}, diff --git a/test/schema/test_types.py b/test/schema/test_types.py index af47f0d..26da270 100644 --- a/test/schema/test_types.py +++ b/test/schema/test_types.py @@ -14,7 +14,7 @@ from bsfs.schema.types import ROOT_PREDICATE, ROOT_VERTEX, ROOT_FEATURE from bsfs.utils import errors # objects to test -from bsfs.schema.types import _Type, _Vertex, Node, Literal, Predicate, Feature +from bsfs.schema.types import _Type, Vertex, Node, Literal, Predicate, Feature ## code ## @@ -47,8 +47,8 @@ class TestType(unittest.TestCase): self.assertEqual( hash(_Type('Foo', None, foo='bar', bar='foo')), hash(_Type('Foo', None, hello='world', foobar=1234))) - # annotations can be passed to get_child - self.assertDictEqual(_Type('First', foo='bar').get_child('Second', bar='foo').annotations, { + # annotations can be passed to child + self.assertDictEqual(_Type('First', foo='bar').child('Second', bar='foo').annotations, { 'bar': 'foo'}) def test_string_conversion(self): @@ -71,16 +71,16 @@ class TestType(unittest.TestCase): self.assertEqual(str(_Type('Foo', SubType('Bar'))), '_Type(Foo)') self.assertEqual(repr(_Type('Foo', SubType('Bar'))), '_Type(Foo, SubType(Bar, None))') - def test_get_child(self): + def test_child(self): # callee is used as parent - self.assertEqual(_Type('First').get_child('Second'), _Type('Second', _Type('First'))) + self.assertEqual(_Type('First').child('Second'), _Type('Second', _Type('First'))) # works with multiple parents - self.assertEqual(_Type('First').get_child('Second').get_child('Third'), _Type('Third', _Type('Second', _Type('First')))) + self.assertEqual(_Type('First').child('Second').child('Third'), _Type('Third', _Type('Second', _Type('First')))) # type persists class Foo(_Type): pass - self.assertEqual(Foo('First').get_child('Second'), Foo('Second', Foo('First'))) + self.assertEqual(Foo('First').child('Second'), Foo('Second', Foo('First'))) # annotations are respected - self.assertDictEqual(_Type('First', foo='bar').get_child('Second', bar='foo').annotations, { + self.assertDictEqual(_Type('First', foo='bar').child('Second', bar='foo').annotations, { 'bar': 'foo'}) def test_equality(self): @@ -174,7 +174,7 @@ class TestPredicate(unittest.TestCase): # range must be a Literal, a Node, or the root Vertex self.assertRaises(TypeError, Predicate, ns.bse.foo, None, Node(ns.bsfs.Node, None), None, True) self.assertRaises(TypeError, Predicate, ns.bse.foo, None, Node(ns.bsfs.Node, None), 1234, True) - self.assertRaises(TypeError, Predicate, ns.bse.foo, None, Node(ns.bsfs.Node, None), _Vertex(ns.bsfs.Foo, None), True) + self.assertRaises(TypeError, Predicate, ns.bse.foo, None, Node(ns.bsfs.Node, None), Vertex(ns.bsfs.Foo, None), True) self.assertRaises(TypeError, Predicate, ns.bse.foo, None, Node(ns.bsfs.Node, None), _Type(ns.bsfs.Foo, None), True) class Foo(): pass self.assertRaises(TypeError, Predicate, ns.bse.foo, None, Node(ns.bsfs.Node, None), Foo(), True) @@ -213,7 +213,7 @@ class TestPredicate(unittest.TestCase): self.assertNotEqual(tag, Predicate(ns.bse.tag, root, n_root, n_tag, True)) self.assertNotEqual(hash(tag), hash(Predicate(ns.bse.tag, root, n_root, n_tag, True))) - def test_get_child(self): + def test_child(self): n_root = Node(ns.bsfs.Node, None) l_root = Literal(ns.bsfs.Literal, None) n_ent = Node(ns.bsfs.Entity, Node(ns.bsfs.Node, None)) @@ -227,45 +227,45 @@ class TestPredicate(unittest.TestCase): unique=False, ) - # get_child returns Predicate - self.assertIsInstance(tag.get_child(ns.bse.foo), Predicate) + # child returns Predicate + self.assertIsInstance(tag.child(ns.bse.foo), Predicate) # uri is respected - self.assertEqual(ns.bse.foo, tag.get_child(ns.bse.foo).uri) + self.assertEqual(ns.bse.foo, tag.child(ns.bse.foo).uri) # domain is respected dom = Node(ns.bsfs.Image, n_ent) - self.assertEqual(dom, tag.get_child(ns.bse.foo, domain=dom).domain) + self.assertEqual(dom, tag.child(ns.bse.foo, domain=dom).domain) # range is respected rng = Node(ns.bsfs.Group, n_tag) - self.assertEqual(rng, tag.get_child(ns.bse.foo, range=rng).range) + self.assertEqual(rng, tag.child(ns.bse.foo, range=rng).range) # cannot set range to None - self.assertEqual(n_tag, tag.get_child(ns.bse.foo, range=None).range) + self.assertEqual(n_tag, tag.child(ns.bse.foo, range=None).range) # unique is respected - self.assertTrue(tag.get_child(ns.bse.foo, unique=True).unique) + self.assertTrue(tag.child(ns.bse.foo, unique=True).unique) # annotations are respected - self.assertDictEqual(tag.get_child(ns.bse.foo, foo='bar', bar=123).annotations, { + self.assertDictEqual(tag.child(ns.bse.foo, foo='bar', bar=123).annotations, { 'foo': 'bar', 'bar': 123, }) # domain is inherited from parent - self.assertEqual(n_root, root.get_child(ns.bse.foo).domain) - self.assertEqual(n_ent, tag.get_child(ns.bse.foo).domain) + self.assertEqual(n_root, root.child(ns.bse.foo).domain) + self.assertEqual(n_ent, tag.child(ns.bse.foo).domain) # range is inherited from parent - self.assertEqual(ROOT_VERTEX, root.get_child(ns.bse.foo).range) - self.assertEqual(n_tag, tag.get_child(ns.bse.foo).range) + self.assertEqual(ROOT_VERTEX, root.child(ns.bse.foo).range) + self.assertEqual(n_tag, tag.child(ns.bse.foo).range) # uniqueness is inherited from parent - self.assertFalse(tag.get_child(ns.bse.foo).unique) + self.assertFalse(tag.child(ns.bse.foo).unique) # domain must be subtype of parent's domain - self.assertRaises(errors.ConsistencyError, tag.get_child, ns.bse.foo, domain=n_root) - self.assertRaises(errors.ConsistencyError, tag.get_child, ns.bse.foo, domain=Node(ns.bsfs.Image, n_root)) + self.assertRaises(errors.ConsistencyError, tag.child, ns.bse.foo, domain=n_root) + self.assertRaises(errors.ConsistencyError, tag.child, ns.bse.foo, domain=Node(ns.bsfs.Image, n_root)) # range must be subtype of parent's range - self.assertRaises(errors.ConsistencyError, tag.get_child, ns.bse.foo, range=n_root) - self.assertRaises(errors.ConsistencyError, tag.get_child, ns.bse.foo, range=Node(ns.bsfs.Image, n_root)) - self.assertRaises(TypeError, tag.get_child, ns.bse.foo, range=Literal(ns.bsfs.Tag, l_root)) + self.assertRaises(errors.ConsistencyError, tag.child, ns.bse.foo, range=n_root) + self.assertRaises(errors.ConsistencyError, tag.child, ns.bse.foo, range=Node(ns.bsfs.Image, n_root)) + self.assertRaises(TypeError, tag.child, ns.bse.foo, range=Literal(ns.bsfs.Tag, l_root)) # range can be subtyped from ROOT_VERTEX to Node or Literal - self.assertEqual(n_root, root.get_child(ns.bse.foo, range=n_root).range) - self.assertEqual(l_root, root.get_child(ns.bse.foo, range=l_root).range) + self.assertEqual(n_root, root.child(ns.bse.foo, range=n_root).range) + self.assertEqual(l_root, root.child(ns.bse.foo, range=l_root).range) class TestFeature(unittest.TestCase): @@ -308,7 +308,7 @@ class TestFeature(unittest.TestCase): self.assertNotEqual(colors, Feature(ns.bse.colors, ROOT_FEATURE, n_ent, l_array, False, 1234, ns.bsfs.float, ns.bsfs.cosine)) self.assertNotEqual(hash(colors), hash(Feature(ns.bse.colors, ROOT_FEATURE, n_ent, l_array, False, 1234, ns.bsfs.float, ns.bsfs.cosine))) - def test_get_child(self): + def test_child(self): n_root = Node(ns.bsfs.Node, None) n_ent = Node(ns.bsfs.Entity, n_root) l_root = Literal(ns.bsfs.Literal, None) @@ -324,53 +324,53 @@ class TestFeature(unittest.TestCase): distance=ns.bsfs.euclidean, ) - # get_child returns Feature - self.assertIsInstance(colors.get_child(ns.bse.foo), Feature) + # child returns Feature + self.assertIsInstance(colors.child(ns.bse.foo), Feature) # uri is respected - self.assertEqual(ns.bse.foo, colors.get_child(ns.bse.foo).uri) + self.assertEqual(ns.bse.foo, colors.child(ns.bse.foo).uri) # domain is respected dom = Node(ns.bsfs.Image, n_ent) - self.assertEqual(dom, colors.get_child(ns.bse.foo, domain=dom).domain) + self.assertEqual(dom, colors.child(ns.bse.foo, domain=dom).domain) # range is respected rng = Literal(ns.bse.foo, l_array) - self.assertEqual(rng, colors.get_child(ns.bse.foo, range=rng).range) + self.assertEqual(rng, colors.child(ns.bse.foo, range=rng).range) # cannot set range to None - self.assertEqual(l_array, colors.get_child(ns.bse.foo, range=None).range) + self.assertEqual(l_array, colors.child(ns.bse.foo, range=None).range) # unique is respected - self.assertTrue(colors.get_child(ns.bse.foo, unique=True).unique) + self.assertTrue(colors.child(ns.bse.foo, unique=True).unique) # dimension is respected - self.assertEqual(4321, colors.get_child(ns.bse.foo, dimension=4321).dimension) + self.assertEqual(4321, colors.child(ns.bse.foo, dimension=4321).dimension) # dtype is respected - self.assertEqual(ns.bsfs.integer, colors.get_child(ns.bse.foo, dtype=ns.bsfs.integer).dtype) + self.assertEqual(ns.bsfs.integer, colors.child(ns.bse.foo, dtype=ns.bsfs.integer).dtype) # distance is respected - self.assertEqual(ns.bsfs.cosine, colors.get_child(ns.bse.foo, distance=ns.bsfs.cosine).distance) + self.assertEqual(ns.bsfs.cosine, colors.child(ns.bse.foo, distance=ns.bsfs.cosine).distance) # annotations are respected - self.assertDictEqual(colors.get_child(ns.bse.foo, foo='bar', bar=123).annotations, { + self.assertDictEqual(colors.child(ns.bse.foo, foo='bar', bar=123).annotations, { 'foo': 'bar', 'bar': 123, }) # domain is inherited from parent - self.assertEqual(n_root, ROOT_FEATURE.get_child(ns.bse.foo).domain) - self.assertEqual(n_ent, colors.get_child(ns.bse.foo).domain) + self.assertEqual(n_root, ROOT_FEATURE.child(ns.bse.foo).domain) + self.assertEqual(n_ent, colors.child(ns.bse.foo).domain) # range is inherited from parent - self.assertEqual(l_array, colors.get_child(ns.bse.foo).range) + self.assertEqual(l_array, colors.child(ns.bse.foo).range) # uniqueness is inherited from parent - self.assertFalse(colors.get_child(ns.bse.foo).unique) + self.assertFalse(colors.child(ns.bse.foo).unique) # dimension is inherited from parent - self.assertEqual(1234, colors.get_child(ns.bse.foo).dimension) + self.assertEqual(1234, colors.child(ns.bse.foo).dimension) # dtype is inherited from parent - self.assertEqual(ns.bsfs.float, colors.get_child(ns.bse.foo).dtype) + self.assertEqual(ns.bsfs.float, colors.child(ns.bse.foo).dtype) # distance is inherited from parent - self.assertEqual(ns.bsfs.euclidean, colors.get_child(ns.bse.foo).distance) + self.assertEqual(ns.bsfs.euclidean, colors.child(ns.bse.foo).distance) # domain must be subtype of parent's domain - self.assertRaises(errors.ConsistencyError, colors.get_child, ns.bse.foo, domain=n_root) - self.assertRaises(errors.ConsistencyError, colors.get_child, ns.bse.foo, domain=Node(ns.bsfs.Image, n_root)) + self.assertRaises(errors.ConsistencyError, colors.child, ns.bse.foo, domain=n_root) + self.assertRaises(errors.ConsistencyError, colors.child, ns.bse.foo, domain=Node(ns.bsfs.Image, n_root)) # range must be subtype of parent's range - self.assertRaises(errors.ConsistencyError, colors.get_child, ns.bse.foo, range=Literal(ns.bsfs.Literal, None)) - self.assertRaises(errors.ConsistencyError, colors.get_child, ns.bse.foo, range=Literal(ns.bsfs.foo, Literal(ns.bsfs.Literal, None))) - self.assertRaises(TypeError, colors.get_child, ns.bse.foo, range=Node(ns.bsfs.Tag, n_root)) + self.assertRaises(errors.ConsistencyError, colors.child, ns.bse.foo, range=Literal(ns.bsfs.Literal, None)) + self.assertRaises(errors.ConsistencyError, colors.child, ns.bse.foo, range=Literal(ns.bsfs.foo, Literal(ns.bsfs.Literal, None))) + self.assertRaises(TypeError, colors.child, ns.bse.foo, range=Node(ns.bsfs.Tag, n_root)) ## main ## diff --git a/test/triple_store/sparql/test_parse_filter.py b/test/triple_store/sparql/test_parse_filter.py index bd19803..bd967e5 100644 --- a/test/triple_store/sparql/test_parse_filter.py +++ b/test/triple_store/sparql/test_parse_filter.py @@ -124,7 +124,7 @@ class TestParseFilter(unittest.TestCase): # __call__ requires a valid root type self.assertRaises(errors.BackendError, self.parser, self.schema.literal(ns.bsfs.Literal), None) - self.assertRaises(errors.ConsistencyError, self.parser, self.schema.node(ns.bsfs.Node).get_child(ns.bsfs.Invalid), None) + self.assertRaises(errors.ConsistencyError, self.parser, self.schema.node(ns.bsfs.Node).child(ns.bsfs.Invalid), None) # __call__ requires a parseable root self.assertRaises(errors.BackendError, self.parser, self.schema.node(ns.bsfs.Entity), ast.filter.FilterExpression()) # __call__ returns an executable query diff --git a/test/triple_store/sparql/test_sparql.py b/test/triple_store/sparql/test_sparql.py index 3d81de1..25a0b15 100644 --- a/test/triple_store/sparql/test_sparql.py +++ b/test/triple_store/sparql/test_sparql.py @@ -108,7 +108,7 @@ class TestSparqlStore(unittest.TestCase): store.create(store.schema.node(ns.bsfs.PDF), {URI('http://example.com/me/pdf#1234')}) # node_type must be in the schema - self.assertRaises(errors.ConsistencyError, store._has_type, URI('http://example.com/me/entity#1234'), store.schema.node(ns.bsfs.Node).get_child(ns.bsfs.invalid)) + self.assertRaises(errors.ConsistencyError, store._has_type, URI('http://example.com/me/entity#1234'), store.schema.node(ns.bsfs.Node).child(ns.bsfs.invalid)) # returns False on inexistent nodes self.assertFalse(store._has_type(URI('http://example.com/me/entity#4321'), store.schema.node(ns.bsfs.Entity))) @@ -509,7 +509,7 @@ class TestSparqlStore(unittest.TestCase): store.set(ent_type, {URI('http://example.com/me/entity#1234')}, self.schema.predicate(ns.bse.filesize), {1234}) store.set(ent_type, {URI('http://example.com/me/entity#4321')}, self.schema.predicate(ns.bse.filesize), {4321}) # node_type must be in the schema - self.assertRaises(errors.ConsistencyError, set, store.get(self.schema.node(ns.bsfs.Node).get_child(ns.bsfs.Invalid), ast.filter.IsIn(ent_ids))) + self.assertRaises(errors.ConsistencyError, set, store.get(self.schema.node(ns.bsfs.Node).child(ns.bsfs.Invalid), ast.filter.IsIn(ent_ids))) # query must be a filter expression class Foo(): pass self.assertRaises(TypeError, set, store.get(ent_type, 1234)) @@ -574,7 +574,7 @@ class TestSparqlStore(unittest.TestCase): store.schema = self.schema # node type must be valid - self.assertRaises(errors.ConsistencyError, store.create, self.schema.node(ns.bsfs.Entity).get_child(ns.bsfs.invalid), { + self.assertRaises(errors.ConsistencyError, store.create, self.schema.node(ns.bsfs.Entity).child(ns.bsfs.invalid), { URI('http://example.com/me/entity#1234'), URI('http://example.com/me/entity#4321')}) # can create some nodes @@ -636,7 +636,7 @@ class TestSparqlStore(unittest.TestCase): p_comment = store.schema.predicate(ns.bse.comment) p_author = store.schema.predicate(ns.bse.author) p_tag = store.schema.predicate(ns.bse.tag) - p_invalid = store.schema.predicate(ns.bsfs.Predicate).get_child(ns.bsfs.foo, range=store.schema.node(ns.bsfs.Tag)) + p_invalid = store.schema.predicate(ns.bsfs.Predicate).child(ns.bsfs.foo, range=store.schema.node(ns.bsfs.Tag)) # create node instances ent_ids = { URI('http://example.com/me/entity#1234'), @@ -659,7 +659,7 @@ class TestSparqlStore(unittest.TestCase): store.create(user_type, user_ids) # invalid node_type is not permitted - self.assertRaises(errors.ConsistencyError, store.set, self.schema.node(ns.bsfs.Node).get_child(ns.bse.foo), + self.assertRaises(errors.ConsistencyError, store.set, self.schema.node(ns.bsfs.Node).child(ns.bse.foo), ent_ids, p_comment, {'hello world'}) # invalid predicate is not permitted -- cgit v1.2.3 From 3940cb3c79937a431ba2ae3b57fd0c6c2ccfff33 Mon Sep 17 00:00:00 2001 From: Matthias Baumgartner Date: Thu, 12 Jan 2023 10:12:43 +0100 Subject: use Vertex in type annotations --- bsfs/graph/resolve.py | 28 +++++++++++------------ bsfs/query/validator.py | 25 +++++++++----------- bsfs/triple_store/sparql/parse_filter.py | 39 +++++++++++++++----------------- 3 files changed, 42 insertions(+), 50 deletions(-) diff --git a/bsfs/graph/resolve.py b/bsfs/graph/resolve.py index feb0855..e398a5e 100644 --- a/bsfs/graph/resolve.py +++ b/bsfs/graph/resolve.py @@ -37,8 +37,6 @@ class Filter(): """ - T_VERTEX = typing.Union[bsc.Node, bsc.Literal] - def __init__(self, schema): self.schema = schema @@ -47,7 +45,7 @@ class Filter(): def _parse_filter_expression( self, - type_: T_VERTEX, + type_: bsc.Vertex, node: ast.filter.FilterExpression, ) -> ast.filter.FilterExpression: """Route *node* to the handler of the respective FilterExpression subclass.""" @@ -73,7 +71,7 @@ class Filter(): # invalid node raise errors.BackendError(f'expected filter expression, found {node}') - def _parse_predicate_expression(self, node: ast.filter.PredicateExpression) -> T_VERTEX: + def _parse_predicate_expression(self, node: ast.filter.PredicateExpression) -> bsc.Vertex: """Route *node* to the handler of the respective PredicateExpression subclass.""" if isinstance(node, ast.filter.Predicate): return self._predicate(node) @@ -82,7 +80,7 @@ class Filter(): # invalid node raise errors.BackendError(f'expected predicate expression, found {node}') - def _predicate(self, node: ast.filter.Predicate) -> T_VERTEX: + def _predicate(self, node: ast.filter.Predicate) -> bsc.Vertex: if not self.schema.has_predicate(node.predicate): raise errors.ConsistencyError(f'predicate {node.predicate} is not in the schema') pred = self.schema.predicate(node.predicate) @@ -91,7 +89,7 @@ class Filter(): dom, rng = rng, dom return rng - def _one_of(self, node: ast.filter.OneOf) -> T_VERTEX: + def _one_of(self, node: ast.filter.OneOf) -> bsc.Vertex: # determine domain and range types rng = None for pred in node: @@ -107,33 +105,33 @@ class Filter(): raise errors.UnreachableError() return rng - def _any(self, type_: T_VERTEX, node: ast.filter.Any) -> ast.filter.Any: # pylint: disable=unused-argument + def _any(self, type_: bsc.Vertex, node: ast.filter.Any) -> ast.filter.Any: # pylint: disable=unused-argument next_type = self._parse_predicate_expression(node.predicate) return ast.filter.Any(node.predicate, self._parse_filter_expression(next_type, node.expr)) - def _all(self, type_: T_VERTEX, node: ast.filter.All) -> ast.filter.All: # pylint: disable=unused-argument + def _all(self, type_: bsc.Vertex, node: ast.filter.All) -> ast.filter.All: # pylint: disable=unused-argument next_type = self._parse_predicate_expression(node.predicate) return ast.filter.All(node.predicate, self._parse_filter_expression(next_type, node.expr)) - def _and(self, type_: T_VERTEX, node: ast.filter.And) -> ast.filter.And: + def _and(self, type_: bsc.Vertex, node: ast.filter.And) -> ast.filter.And: return ast.filter.And({self._parse_filter_expression(type_, expr) for expr in node}) - def _or(self, type_: T_VERTEX, node: ast.filter.Or) -> ast.filter.Or: + def _or(self, type_: bsc.Vertex, node: ast.filter.Or) -> ast.filter.Or: return ast.filter.Or({self._parse_filter_expression(type_, expr) for expr in node}) - def _not(self, type_: T_VERTEX, node: ast.filter.Not) -> ast.filter.Not: + def _not(self, type_: bsc.Vertex, node: ast.filter.Not) -> ast.filter.Not: return ast.filter.Not(self._parse_filter_expression(type_, node.expr)) - def _has(self, type_: T_VERTEX, node: ast.filter.Has) -> ast.filter.Has: # pylint: disable=unused-argument + def _has(self, type_: bsc.Vertex, node: ast.filter.Has) -> ast.filter.Has: # pylint: disable=unused-argument return node - def _value(self, type_: T_VERTEX, node: ast.filter._Value) -> ast.filter._Value: # pylint: disable=unused-argument + def _value(self, type_: bsc.Vertex, node: ast.filter._Value) -> ast.filter._Value: # pylint: disable=unused-argument return node - def _bounded(self, type_: T_VERTEX, node: ast.filter._Bounded) -> ast.filter._Bounded: # pylint: disable=unused-argument + def _bounded(self, type_: bsc.Vertex, node: ast.filter._Bounded) -> ast.filter._Bounded: # pylint: disable=unused-argument return node - def _is(self, type_: T_VERTEX, node: ast.filter.Is) -> typing.Union[ast.filter.Or, ast.filter.Is]: + def _is(self, type_: bsc.Vertex, node: ast.filter.Is) -> typing.Union[ast.filter.Or, ast.filter.Is]: # check if action is needed if not isinstance(node.value, nodes.Nodes): return node diff --git a/bsfs/query/validator.py b/bsfs/query/validator.py index 352203a..6bf1b72 100644 --- a/bsfs/query/validator.py +++ b/bsfs/query/validator.py @@ -34,9 +34,6 @@ class Filter(): """ - # vertex types - T_VERTEX = typing.Union[bsc.Node, bsc.Literal] # FIXME: Shouldn't this be in the schema? - # schema to validate against. schema: bsc.Schema @@ -64,7 +61,7 @@ class Filter(): ## routing methods - def _parse_filter_expression(self, type_: T_VERTEX, node: ast.filter.FilterExpression): + def _parse_filter_expression(self, type_: bsc.Vertex, node: ast.filter.FilterExpression): """Route *node* to the handler of the respective FilterExpression subclass.""" if isinstance(node, ast.filter.Is): return self._is(type_, node) @@ -83,7 +80,7 @@ class Filter(): # invalid node raise errors.BackendError(f'expected filter expression, found {node}') - def _parse_predicate_expression(self, node: ast.filter.PredicateExpression) -> typing.Tuple[T_VERTEX, T_VERTEX]: + def _parse_predicate_expression(self, node: ast.filter.PredicateExpression) -> typing.Tuple[bsc.Vertex, bsc.Vertex]: """Route *node* to the handler of the respective PredicateExpression subclass.""" if isinstance(node, ast.filter.Predicate): return self._predicate(node) @@ -95,7 +92,7 @@ class Filter(): ## predicate expressions - def _predicate(self, node: ast.filter.Predicate) -> typing.Tuple[T_VERTEX, T_VERTEX]: + def _predicate(self, node: ast.filter.Predicate) -> typing.Tuple[bsc.Vertex, bsc.Vertex]: # predicate exists in the schema if not self.schema.has_predicate(node.predicate): raise errors.ConsistencyError(f'predicate {node.predicate} is not in the schema') @@ -110,7 +107,7 @@ class Filter(): # return domain and range return dom, rng - def _one_of(self, node: ast.filter.OneOf) -> typing.Tuple[T_VERTEX, T_VERTEX]: + def _one_of(self, node: ast.filter.OneOf) -> typing.Tuple[bsc.Vertex, bsc.Vertex]: # determine domain and range types # NOTE: select the most specific domain and the most generic range dom, rng = None, None @@ -146,7 +143,7 @@ class Filter(): ## intermediates - def _branch(self, type_: T_VERTEX, node: ast.filter._Branch): + def _branch(self, type_: bsc.Vertex, node: ast.filter._Branch): # type is a Node if not isinstance(type_, bsc.Node): raise errors.ConsistencyError(f'expected a Node, found {type_}') @@ -167,16 +164,16 @@ class Filter(): # child expression is valid self._parse_filter_expression(rng, node.expr) - def _agg(self, type_: T_VERTEX, node: ast.filter._Agg): + def _agg(self, type_: bsc.Vertex, node: ast.filter._Agg): for expr in node: # child expression is valid self._parse_filter_expression(type_, expr) - def _not(self, type_: T_VERTEX, node: ast.filter.Not): + def _not(self, type_: bsc.Vertex, node: ast.filter.Not): # child expression is valid self._parse_filter_expression(type_, node.expr) - def _has(self, type_: T_VERTEX, node: ast.filter.Has): + def _has(self, type_: bsc.Vertex, node: ast.filter.Has): # type is a Node if not isinstance(type_, bsc.Node): raise errors.ConsistencyError(f'expected a Node, found {type_}') @@ -195,13 +192,13 @@ class Filter(): ## conditions - def _is(self, type_: T_VERTEX, node: ast.filter.Is): # pylint: disable=unused-argument # (node) + def _is(self, type_: bsc.Vertex, node: ast.filter.Is): # pylint: disable=unused-argument # (node) if not isinstance(type_, bsc.Node): raise errors.ConsistencyError(f'expected a Node, found {type_}') if type_ not in self.schema.nodes(): raise errors.ConsistencyError(f'node {type_} is not in the schema') - def _value(self, type_: T_VERTEX, node: ast.filter._Value): # pylint: disable=unused-argument # (node) + def _value(self, type_: bsc.Vertex, node: ast.filter._Value): # pylint: disable=unused-argument # (node) # type is a literal if not isinstance(type_, bsc.Literal): raise errors.ConsistencyError(f'expected a Literal, found {type_}') @@ -211,7 +208,7 @@ class Filter(): # FIXME: Check if node.value corresponds to type_ # FIXME: A specific literal might be requested (i.e., a numeric type when used in Has) - def _bounded(self, type_: T_VERTEX, node: ast.filter._Bounded): # pylint: disable=unused-argument # (node) + def _bounded(self, type_: bsc.Vertex, node: ast.filter._Bounded): # pylint: disable=unused-argument # (node) # type is a literal if not isinstance(type_, bsc.Literal): raise errors.ConsistencyError(f'expected a Literal, found {type_}') diff --git a/bsfs/triple_store/sparql/parse_filter.py b/bsfs/triple_store/sparql/parse_filter.py index d4db0aa..a851888 100644 --- a/bsfs/triple_store/sparql/parse_filter.py +++ b/bsfs/triple_store/sparql/parse_filter.py @@ -46,9 +46,6 @@ class Filter(): # Generator that produces unique symbol names. ngen: _GenHopName - # Vertex type. - T_VERTEX = typing.Union[bsc.Node, bsc.Literal] - def __init__(self, schema): self.schema = schema self.ngen = _GenHopName() @@ -79,7 +76,7 @@ class Filter(): }} ''' - def _parse_filter_expression(self, type_: T_VERTEX, node: ast.filter.FilterExpression, head: str) -> str: + def _parse_filter_expression(self, type_: bsc.Vertex, node: ast.filter.FilterExpression, head: str) -> str: """Route *node* to the handler of the respective FilterExpression subclass.""" if isinstance(node, ast.filter.Is): return self._is(type_, node, head) @@ -112,9 +109,9 @@ class Filter(): def _parse_predicate_expression( self, - type_: T_VERTEX, + type_: bsc.Vertex, node: ast.filter.PredicateExpression - ) -> typing.Tuple[str, T_VERTEX]: + ) -> typing.Tuple[str, bsc.Vertex]: """Route *node* to the handler of the respective PredicateExpression subclass.""" if isinstance(node, ast.filter.Predicate): return self._predicate(type_, node) @@ -123,7 +120,7 @@ class Filter(): # invalid node raise errors.BackendError(f'expected predicate expression, found {node}') - def _one_of(self, node_type: T_VERTEX, node: ast.filter.OneOf) -> typing.Tuple[str, T_VERTEX]: + def _one_of(self, node_type: bsc.Vertex, node: ast.filter.OneOf) -> typing.Tuple[str, bsc.Vertex]: """ """ if not isinstance(node_type, bsc.Node): @@ -150,7 +147,7 @@ class Filter(): # return joint predicate expression and next range return '|'.join(suburi), rng - def _predicate(self, node_type: T_VERTEX, node: ast.filter.Predicate) -> typing.Tuple[str, T_VERTEX]: + def _predicate(self, node_type: bsc.Vertex, node: ast.filter.Predicate) -> typing.Tuple[str, bsc.Vertex]: """ """ # check node_type @@ -178,7 +175,7 @@ class Filter(): # return predicate URI and next node type return puri, rng - def _any(self, node_type: T_VERTEX, node: ast.filter.Any, head: str) -> str: + def _any(self, node_type: bsc.Vertex, node: ast.filter.Any, head: str) -> str: """ """ if not isinstance(node_type, bsc.Node): @@ -191,7 +188,7 @@ class Filter(): # combine results return f'{head} {pred} {nexthead} . {expr}' - def _all(self, node_type: T_VERTEX, node: ast.filter.All, head: str) -> str: + def _all(self, node_type: bsc.Vertex, node: ast.filter.All, head: str) -> str: """ """ # NOTE: All(P, E) := Not(Any(P, Not(E))) and EXISTS(P, ?) @@ -208,13 +205,13 @@ class Filter(): # return existence and rewritten expression return f'FILTER EXISTS {{ {head} {pred} {temphead} }} . ' + expr - def _and(self, node_type: T_VERTEX, node: ast.filter.And, head: str) -> str: + def _and(self, node_type: bsc.Vertex, node: ast.filter.And, head: str) -> str: """ """ sub = [self._parse_filter_expression(node_type, expr, head) for expr in node] return ' . '.join(sub) - def _or(self, node_type: T_VERTEX, node: ast.filter.Or, head: str) -> str: + def _or(self, node_type: bsc.Vertex, node: ast.filter.Or, head: str) -> str: """ """ # potential special case optimization: @@ -224,7 +221,7 @@ class Filter(): sub = ['{' + expr + '}' for expr in sub] return ' UNION '.join(sub) - def _not(self, node_type: T_VERTEX, node: ast.filter.Not, head: str) -> str: + def _not(self, node_type: bsc.Vertex, node: ast.filter.Not, head: str) -> str: """ """ expr = self._parse_filter_expression(node_type, node.expr, head) @@ -235,7 +232,7 @@ class Filter(): # The simplest (and non-interfering) choice is a type statement. return f'MINUS {{ {head} <{ns.rdf.type}>/<{ns.rdfs.subClassOf}>* <{node_type.uri}> . {expr} }}' - def _has(self, node_type: T_VERTEX, node: ast.filter.Has, head: str) -> str: + def _has(self, node_type: bsc.Vertex, node: ast.filter.Has, head: str) -> str: """ """ if not isinstance(node_type, bsc.Node): @@ -253,42 +250,42 @@ class Filter(): # combine return num_preds + ' . ' + count_bounds - def _is(self, node_type: T_VERTEX, node: ast.filter.Is, head: str) -> str: + def _is(self, node_type: bsc.Vertex, node: ast.filter.Is, head: str) -> str: """ """ if not isinstance(node_type, bsc.Node): raise errors.BackendError(f'expected Node, found {node_type}') return f'VALUES {head} {{ <{node.value}> }}' - def _equals(self, node_type: T_VERTEX, node: ast.filter.Equals, head: str) -> str: + def _equals(self, node_type: bsc.Vertex, node: ast.filter.Equals, head: str) -> str: """ """ if not isinstance(node_type, bsc.Literal): raise errors.BackendError(f'expected Literal, found {node}') return f'VALUES {head} {{ "{node.value}"^^<{node_type.uri}> }}' - def _substring(self, node_type: T_VERTEX, node: ast.filter.Substring, head: str) -> str: + def _substring(self, node_type: bsc.Vertex, node: ast.filter.Substring, head: str) -> str: """ """ if not isinstance(node_type, bsc.Literal): raise errors.BackendError(f'expected Literal, found {node_type}') return f'FILTER contains(str({head}), "{node.value}")' - def _starts_with(self, node_type: T_VERTEX, node: ast.filter.StartsWith, head: str) -> str: + def _starts_with(self, node_type: bsc.Vertex, node: ast.filter.StartsWith, head: str) -> str: """ """ if not isinstance(node_type, bsc.Literal): raise errors.BackendError(f'expected Literal, found {node_type}') return f'FILTER strstarts(str({head}), "{node.value}")' - def _ends_with(self, node_type: T_VERTEX, node: ast.filter.EndsWith, head: str) -> str: + def _ends_with(self, node_type: bsc.Vertex, node: ast.filter.EndsWith, head: str) -> str: """ """ if not isinstance(node_type, bsc.Literal): raise errors.BackendError(f'expected Literal, found {node_type}') return f'FILTER strends(str({head}), "{node.value}")' - def _less_than(self, node_type: T_VERTEX, node: ast.filter.LessThan, head: str) -> str: + def _less_than(self, node_type: bsc.Vertex, node: ast.filter.LessThan, head: str) -> str: """ """ if not isinstance(node_type, bsc.Literal): @@ -296,7 +293,7 @@ class Filter(): equality = '=' if not node.strict else '' return f'FILTER ({head} <{equality} {float(node.threshold)})' - def _greater_than(self, node_type: T_VERTEX, node: ast.filter.GreaterThan, head: str) -> str: + def _greater_than(self, node_type: bsc.Vertex, node: ast.filter.GreaterThan, head: str) -> str: """ """ if not isinstance(node_type, bsc.Literal): -- cgit v1.2.3 From 6b3e32b29799a8143e8ce9d20c5f27e3e166b9bb Mon Sep 17 00:00:00 2001 From: Matthias Baumgartner Date: Thu, 12 Jan 2023 10:17:07 +0100 Subject: changed path to from_string in clients --- bsfs/apps/migrate.py | 6 +++--- bsfs/graph/graph.py | 10 +++++----- bsfs/triple_store/sparql/sparql.py | 2 +- test/apps/test_migrate.py | 10 +++++----- test/graph/ac/test_null.py | 4 ++-- test/graph/test_graph.py | 12 ++++++------ test/graph/test_nodes.py | 2 +- test/graph/test_resolve.py | 2 +- test/query/test_validator.py | 2 +- test/triple_store/sparql/test_parse_filter.py | 4 ++-- test/triple_store/sparql/test_sparql.py | 14 +++++++------- 11 files changed, 34 insertions(+), 34 deletions(-) diff --git a/bsfs/apps/migrate.py b/bsfs/apps/migrate.py index 91c1661..b9d019f 100644 --- a/bsfs/apps/migrate.py +++ b/bsfs/apps/migrate.py @@ -42,15 +42,15 @@ def main(argv): graph = bsfs.Open(config) # initialize schema - schema = bsfs.schema.Schema.Empty() + schema = bsfs.schema.Schema() if len(args.schema) == 0: # assemble schema from standard input - schema = schema + bsfs.schema.Schema.from_string(sys.stdin.read()) + schema = schema + bsfs.schema.from_string(sys.stdin.read()) else: # assemble schema from input files for pth in args.schema: with open(pth, mode='rt', encoding='UTF-8') as ifile: - schema = schema + bsfs.schema.Schema.from_string(ifile.read()) + schema = schema + bsfs.schema.from_string(ifile.read()) # migrate schema graph.migrate(schema, not args.remove) diff --git a/bsfs/graph/graph.py b/bsfs/graph/graph.py index f030fed..2210755 100644 --- a/bsfs/graph/graph.py +++ b/bsfs/graph/graph.py @@ -10,7 +10,7 @@ import typing # bsfs imports from bsfs.query import ast, validate -from bsfs.schema import Schema +from bsfs import schema as bsc from bsfs.triple_store import TripleStoreBase from bsfs.utils import URI, typename @@ -67,11 +67,11 @@ class Graph(): return f'{typename(self)}({str(self._backend)}, {self._user})' @property - def schema(self) -> Schema: + def schema(self) -> bsc.Schema: """Return the store's local schema.""" return self._backend.schema - def migrate(self, schema: Schema, append: bool = True) -> 'Graph': + def migrate(self, schema: bsc.Schema, append: bool = True) -> 'Graph': """Migrate the current schema to a new *schema*. Appends to the current schema by default; control this via *append*. @@ -79,14 +79,14 @@ class Graph(): """ # check args - if not isinstance(schema, Schema): + if not isinstance(schema, bsc.Schema): raise TypeError(schema) # append to current schema if append: schema = schema + self._backend.schema # add Graph schema requirements with open(os.path.join(os.path.dirname(__file__), 'schema.nt'), mode='rt', encoding='UTF-8') as ifile: - schema = schema + Schema.from_string(ifile.read()) + schema = schema + bsc.from_string(ifile.read()) # migrate schema in backend # FIXME: consult access controls! self._backend.schema = schema diff --git a/bsfs/triple_store/sparql/sparql.py b/bsfs/triple_store/sparql/sparql.py index c3cbff6..ddace35 100644 --- a/bsfs/triple_store/sparql/sparql.py +++ b/bsfs/triple_store/sparql/sparql.py @@ -94,7 +94,7 @@ class SparqlStore(base.TripleStoreBase): super().__init__(None) self._graph = rdflib.Graph() self._transaction = _Transaction(self._graph) - self._schema = bsc.Schema.Empty() + self._schema = bsc.Schema() self._filter_parser = parse_filter.Filter(self._schema) # NOTE: mypy and pylint complain about the **kwargs not being listed (contrasting super) diff --git a/test/apps/test_migrate.py b/test/apps/test_migrate.py index 957509a..230c032 100644 --- a/test/apps/test_migrate.py +++ b/test/apps/test_migrate.py @@ -13,7 +13,7 @@ import unittest import unittest.mock # bsie imports -from bsfs.schema import Schema +from bsfs import schema # objects to test from bsfs.apps.migrate import main @@ -33,21 +33,21 @@ class TestMigrate(unittest.TestCase): # read schema from file with open(schema_1) as ifile: - target = Schema.from_string(ifile.read()) + target = schema.from_string(ifile.read()) graph = main([config, schema_1]) self.assertTrue(target <= graph.schema) # read schema from multiple files with open(schema_1) as ifile: - target = Schema.from_string(ifile.read()) + target = schema.from_string(ifile.read()) with open(schema_2) as ifile: - target = target + Schema.from_string(ifile.read()) + target = target + schema.from_string(ifile.read()) graph = main([config, schema_1, schema_2]) self.assertTrue(target <= graph.schema) # read schema from stdin with open(schema_1, 'rt') as ifile: - target = Schema.from_string(ifile.read()) + target = schema.from_string(ifile.read()) with open(schema_1, 'rt') as ifile: with unittest.mock.patch('sys.stdin', ifile): graph = main([config]) diff --git a/test/graph/ac/test_null.py b/test/graph/ac/test_null.py index c863943..c3df393 100644 --- a/test/graph/ac/test_null.py +++ b/test/graph/ac/test_null.py @@ -8,7 +8,7 @@ Author: Matthias Baumgartner, 2022 import unittest # bsie imports -from bsfs import schema as _schema +from bsfs import schema as bsc from bsfs.namespace import ns from bsfs.query import ast from bsfs.triple_store import SparqlStore @@ -23,7 +23,7 @@ from bsfs.graph.ac.null import NullAC class TestNullAC(unittest.TestCase): def setUp(self): self.backend = SparqlStore() - self.backend.schema = _schema.Schema.from_string(''' + self.backend.schema = bsc.from_string(''' prefix rdfs: prefix xsd: diff --git a/test/graph/test_graph.py b/test/graph/test_graph.py index 8503d5b..125084c 100644 --- a/test/graph/test_graph.py +++ b/test/graph/test_graph.py @@ -25,7 +25,7 @@ class TestGraph(unittest.TestCase): def setUp(self): self.user = URI('http://example.com/me') self.backend = SparqlStore.Open() - self.backend.schema = schema.Schema.from_string(''' + self.backend.schema = schema.from_string(''' prefix rdfs: prefix bsfs: bsfs:Entity rdfs:subClassOf bsfs:Node . @@ -118,7 +118,7 @@ class TestGraph(unittest.TestCase): schema.Node(ns.bsfs.Node, None)))}), append=False) # can migrate to compatible schema - target_1 = schema.Schema.from_string(''' + target_1 = schema.from_string(''' prefix rdfs: prefix xsd: prefix bsfs: @@ -142,7 +142,7 @@ class TestGraph(unittest.TestCase): # new schema is applied self.assertLess(target_1, graph.schema) # graph appends its predicates - self.assertEqual(graph.schema, target_1 + schema.Schema.from_string(''' + self.assertEqual(graph.schema, target_1 + schema.from_string(''' prefix rdfs: prefix xsd: prefix bsfs: @@ -155,7 +155,7 @@ class TestGraph(unittest.TestCase): ''')) # can overwrite the current schema - target_2 = schema.Schema.from_string(''' + target_2 = schema.from_string(''' prefix rdfs: prefix xsd: prefix bsfs: @@ -181,7 +181,7 @@ class TestGraph(unittest.TestCase): # new schema is applied self.assertLess(target_2, graph.schema) # graph appends its predicates - self.assertEqual(graph.schema, target_2 + schema.Schema.from_string(''' + self.assertEqual(graph.schema, target_2 + schema.from_string(''' prefix rdfs: prefix xsd: prefix bsfs: @@ -196,7 +196,7 @@ class TestGraph(unittest.TestCase): def test_get(self): # setup graph = Graph(self.backend, self.user) - graph.migrate(schema.Schema.from_string(''' + graph.migrate(schema.from_string(''' prefix rdfs: prefix xsd: prefix bsfs: diff --git a/test/graph/test_nodes.py b/test/graph/test_nodes.py index 11ae46d..47647bd 100644 --- a/test/graph/test_nodes.py +++ b/test/graph/test_nodes.py @@ -24,7 +24,7 @@ class TestNodes(unittest.TestCase): def setUp(self): # initialize backend self.backend = SparqlStore() - self.backend.schema = _schema.Schema.from_string(''' + self.backend.schema = _schema.from_string(''' prefix rdfs: prefix xsd: diff --git a/test/graph/test_resolve.py b/test/graph/test_resolve.py index f515320..a27e8c9 100644 --- a/test/graph/test_resolve.py +++ b/test/graph/test_resolve.py @@ -31,7 +31,7 @@ class TestFilter(unittest.TestCase): """ def test_call(self): - schema = bsc.Schema.from_string(''' + schema = bsc.from_string(''' prefix rdfs: prefix xsd: diff --git a/test/query/test_validator.py b/test/query/test_validator.py index bf3ceeb..405872c 100644 --- a/test/query/test_validator.py +++ b/test/query/test_validator.py @@ -21,7 +21,7 @@ from bsfs.query.validator import Filter class TestFilter(unittest.TestCase): def setUp(self): - self.schema = _schema.Schema.from_string(''' + self.schema = _schema.from_string(''' prefix rdfs: prefix xsd: diff --git a/test/triple_store/sparql/test_parse_filter.py b/test/triple_store/sparql/test_parse_filter.py index bd967e5..1d96994 100644 --- a/test/triple_store/sparql/test_parse_filter.py +++ b/test/triple_store/sparql/test_parse_filter.py @@ -9,7 +9,7 @@ import rdflib import unittest # bsie imports -from bsfs import schema as _schema +from bsfs import schema as bsc from bsfs.namespace import ns from bsfs.query import ast from bsfs.utils import errors @@ -23,7 +23,7 @@ from bsfs.triple_store.sparql.parse_filter import Filter class TestParseFilter(unittest.TestCase): def setUp(self): # schema - self.schema = _schema.Schema.from_string(''' + self.schema = bsc.from_string(''' prefix rdfs: prefix xsd: diff --git a/test/triple_store/sparql/test_sparql.py b/test/triple_store/sparql/test_sparql.py index 25a0b15..5342925 100644 --- a/test/triple_store/sparql/test_sparql.py +++ b/test/triple_store/sparql/test_sparql.py @@ -9,7 +9,7 @@ import rdflib import unittest # bsie imports -from bsfs import schema as _schema +from bsfs import schema as bsc from bsfs.namespace import ns from bsfs.query import ast from bsfs.utils import errors, URI @@ -22,7 +22,7 @@ from bsfs.triple_store.sparql.sparql import SparqlStore class TestSparqlStore(unittest.TestCase): def setUp(self): - self.schema = _schema.Schema.from_string(''' + self.schema = bsc.from_string(''' prefix rdfs: prefix xsd: @@ -90,7 +90,7 @@ class TestSparqlStore(unittest.TestCase): def test__has_type(self): # setup store store = SparqlStore.Open() - store.schema = _schema.Schema.from_string(''' + store.schema = bsc.from_string(''' prefix rdfs: prefix xsd: prefix bsfs: @@ -195,7 +195,7 @@ class TestSparqlStore(unittest.TestCase): self.assertSetEqual(set(store._graph), instances) # add some classes to the schema - curr = curr + _schema.Schema.from_string(''' + curr = curr + bsc.from_string(''' prefix rdfs: prefix xsd: prefix bsfs: @@ -298,7 +298,7 @@ class TestSparqlStore(unittest.TestCase): # remove some classes from the schema - curr = _schema.Schema.from_string(''' + curr = bsc.from_string(''' prefix rdfs: prefix xsd: prefix bsfs: @@ -383,7 +383,7 @@ class TestSparqlStore(unittest.TestCase): self.assertRaises(TypeError, setattr, store, 'schema', Foo()) # cannot migrate to incompatible schema - invalid = _schema.Schema.from_string(''' + invalid = bsc.from_string(''' prefix rdfs: prefix xsd: prefix bsfs: @@ -399,7 +399,7 @@ class TestSparqlStore(unittest.TestCase): ''') self.assertRaises(errors.ConsistencyError, setattr, store, 'schema', invalid) - invalid = _schema.Schema.from_string(''' + invalid = bsc.from_string(''' prefix rdfs: prefix xsd: prefix bsfs: -- cgit v1.2.3 From 7e7284d5fc01c0a081aa79d67736f51069864a7d Mon Sep 17 00:00:00 2001 From: Matthias Baumgartner Date: Thu, 12 Jan 2023 10:22:59 +0100 Subject: adapt to non-optional range in query checks --- bsfs/graph/resolve.py | 4 ++-- bsfs/query/validator.py | 14 +++++--------- bsfs/triple_store/sparql/parse_filter.py | 13 +++++-------- 3 files changed, 12 insertions(+), 19 deletions(-) diff --git a/bsfs/graph/resolve.py b/bsfs/graph/resolve.py index e398a5e..9b5f631 100644 --- a/bsfs/graph/resolve.py +++ b/bsfs/graph/resolve.py @@ -101,8 +101,8 @@ class Filter(): rng = subrng except TypeError as err: raise errors.ConsistencyError(f'ranges {subrng} and {rng} are not related') from err - if rng is None: - raise errors.UnreachableError() + if not isinstance(rng, (bsc.Node, bsc.Literal)): + raise errors.BackendError(f'the range of node {node} is undefined') return rng def _any(self, type_: bsc.Vertex, node: ast.filter.Any) -> ast.filter.Any: # pylint: disable=unused-argument diff --git a/bsfs/query/validator.py b/bsfs/query/validator.py index 6bf1b72..b04a9bf 100644 --- a/bsfs/query/validator.py +++ b/bsfs/query/validator.py @@ -98,10 +98,9 @@ class Filter(): raise errors.ConsistencyError(f'predicate {node.predicate} is not in the schema') # determine domain and range pred = self.schema.predicate(node.predicate) + if not isinstance(pred.range, (bsc.Node, bsc.Literal)): + raise errors.BackendError(f'the range of predicate {pred} is undefined') dom, rng = pred.domain, pred.range - if rng is None: - # FIXME: It is a design error that Predicates can have a None range... - raise errors.BackendError(f'predicate {pred} has no range') if node.reverse: dom, rng = rng, dom # type: ignore [assignment] # variable re-use confuses mypy # return domain and range @@ -133,12 +132,9 @@ class Filter(): raise errors.ConsistencyError(f'ranges {subrng} and {rng} are not related') except TypeError as err: # compared literal vs. node raise errors.ConsistencyError(f'ranges {subrng} and {rng} are not of the same type') from err - # check domain and range - if dom is None or rng is None: - # OneOf guarantees at least one expression, these two cases cannot happen - raise errors.UnreachableError() - # return domain and range - return dom, rng + # OneOf guarantees at least one expression, dom and rng are always bsc.Vertex. + # mypy does not realize this, hence we ignore the warning. + return dom, rng # type: ignore [return-value] ## intermediates diff --git a/bsfs/triple_store/sparql/parse_filter.py b/bsfs/triple_store/sparql/parse_filter.py index a851888..0297cbc 100644 --- a/bsfs/triple_store/sparql/parse_filter.py +++ b/bsfs/triple_store/sparql/parse_filter.py @@ -140,12 +140,10 @@ class Filter(): raise errors.ConsistencyError(f'ranges {subrng} and {rng} are not related') except TypeError as err: # subrng and rng are not comparable raise errors.ConsistencyError(f'ranges {subrng} and {rng} are not related') from err - if rng is None: - # for mypy to be certain of the rng type - # if rng were None, we'd have gotten a TypeError above (None > None) - raise errors.UnreachableError() # return joint predicate expression and next range - return '|'.join(suburi), rng + # OneOf guarantees at least one expression, rng is always a bsc.Vertex. + # mypy does not realize this, hence we ignore the warning. + return '|'.join(suburi), rng # type: ignore [return-value] def _predicate(self, node_type: bsc.Vertex, node: ast.filter.Predicate) -> typing.Tuple[str, bsc.Vertex]: """ @@ -159,9 +157,8 @@ class Filter(): if not self.schema.has_predicate(puri): raise errors.ConsistencyError(f'predicate {puri} is not in the schema') pred = self.schema.predicate(puri) - if pred.range is None: - # FIXME: It is a design error that Predicates can have a None range... - raise errors.BackendError(f'predicate {pred} has no range') + if not isinstance(pred.range, (bsc.Node, bsc.Literal)): + raise errors.BackendError(f'the range of predicate {pred} is undefined') dom, rng = pred.domain, pred.range # encapsulate predicate uri puri = f'<{puri}>' # type: ignore [assignment] # variable re-use confuses mypy -- cgit v1.2.3 From b0ff4ed674ad78bf113c3cc0c2ccd187ccb91048 Mon Sep 17 00:00:00 2001 From: Matthias Baumgartner Date: Thu, 12 Jan 2023 10:26:30 +0100 Subject: number literal adaptions --- bsfs/graph/schema.nt | 3 ++- bsfs/query/validator.py | 6 ++++-- bsfs/triple_store/sparql/parse_filter.py | 1 - bsfs/triple_store/sparql/sparql.py | 4 +++- test/apps/schema-2.nt | 3 ++- test/graph/ac/test_null.py | 3 ++- test/graph/test_graph.py | 12 ++++++++---- test/graph/test_nodes.py | 6 ++++-- test/graph/test_resolve.py | 3 ++- test/query/test_validator.py | 6 +++++- test/schema/test_schema.py | 17 +++++++++-------- test/schema/test_serialize.py | 10 ++++++---- test/triple_store/sparql/test_parse_filter.py | 3 ++- test/triple_store/sparql/test_sparql.py | 12 ++++++++---- 14 files changed, 57 insertions(+), 32 deletions(-) diff --git a/bsfs/graph/schema.nt b/bsfs/graph/schema.nt index 8612681..f619746 100644 --- a/bsfs/graph/schema.nt +++ b/bsfs/graph/schema.nt @@ -8,7 +8,8 @@ prefix bsfs: prefix bsm: # literals -xsd:integer rdfs:subClassOf bsfs:Literal . +bsfs:Number rdfs:subClassOf bsfs:Literal . +xsd:integer rdfs:subClassOf bsfs:Number . # predicates bsm:t_created rdfs:subClassOf bsfs:Predicate ; diff --git a/bsfs/query/validator.py b/bsfs/query/validator.py index b04a9bf..75b51ca 100644 --- a/bsfs/query/validator.py +++ b/bsfs/query/validator.py @@ -182,8 +182,7 @@ class Filter(): if not type_ <= dom: raise errors.ConsistencyError(f'expected type {dom}, found {type_}') # node.count is a numerical expression - # FIXME: We have to ensure that ns.xsd.integer is always known in the schema! - self._parse_filter_expression(self.schema.literal(ns.xsd.integer), node.count) + self._parse_filter_expression(self.schema.literal(ns.bsfs.Number), node.count) ## conditions @@ -211,6 +210,9 @@ class Filter(): # type exists in the schema if type_ not in self.schema.literals(): raise errors.ConsistencyError(f'literal {type_} is not in the schema') + # type must be a numerical + if not type_ <= self.schema.literal(ns.bsfs.Number): + raise errors.ConsistencyError(f'expected a number type, found {type_}') # FIXME: Check if node.value corresponds to type_ diff --git a/bsfs/triple_store/sparql/parse_filter.py b/bsfs/triple_store/sparql/parse_filter.py index 0297cbc..18a3288 100644 --- a/bsfs/triple_store/sparql/parse_filter.py +++ b/bsfs/triple_store/sparql/parse_filter.py @@ -242,7 +242,6 @@ class Filter(): # predicate count expression (fetch number of predicates at *head*) num_preds = f'{{ SELECT (COUNT(distinct {inner}) as {outer}) WHERE {{ {head} {pred} {inner} }} }}' # count expression - # FIXME: We have to ensure that ns.xsd.integer is always known in the schema! count_bounds = self._parse_filter_expression(self.schema.literal(ns.xsd.integer), node.count, outer) # combine return num_preds + ' . ' + count_bounds diff --git a/bsfs/triple_store/sparql/sparql.py b/bsfs/triple_store/sparql/sparql.py index ddace35..87467ff 100644 --- a/bsfs/triple_store/sparql/sparql.py +++ b/bsfs/triple_store/sparql/sparql.py @@ -11,6 +11,7 @@ import rdflib # bsfs imports from bsfs import schema as bsc +from bsfs.namespace import ns from bsfs.query import ast from bsfs.utils import errors, URI @@ -94,7 +95,8 @@ class SparqlStore(base.TripleStoreBase): super().__init__(None) self._graph = rdflib.Graph() self._transaction = _Transaction(self._graph) - self._schema = bsc.Schema() + # NOTE: parsing bsfs.query.ast.filter.Has requires xsd:integer. + self._schema = bsc.Schema(literals={bsc.ROOT_NUMBER.child(ns.xsd.integer)}) self._filter_parser = parse_filter.Filter(self._schema) # NOTE: mypy and pylint complain about the **kwargs not being listed (contrasting super) diff --git a/test/apps/schema-2.nt b/test/apps/schema-2.nt index 525ac99..4c5468f 100644 --- a/test/apps/schema-2.nt +++ b/test/apps/schema-2.nt @@ -10,7 +10,8 @@ prefix bse: bsfs:Entity rdfs:subClassOf bsfs:Node . # common definitions -xsd:integer rdfs:subClassOf bsfs:Literal . +bsfs:Number rdfs:subClassOf bsfs:Literal . +xsd:integer rdfs:subClassOf bsfs:Number . bse:filesize rdfs:subClassOf bsfs:Predicate ; rdfs:domain bsfs:Entity ; diff --git a/test/graph/ac/test_null.py b/test/graph/ac/test_null.py index c3df393..e35852d 100644 --- a/test/graph/ac/test_null.py +++ b/test/graph/ac/test_null.py @@ -34,7 +34,8 @@ class TestNullAC(unittest.TestCase): bsfs:Entity rdfs:subClassOf bsfs:Node . bsfs:Tag rdfs:subClassOf bsfs:Node . xsd:string rdfs:subClassOf bsfs:Literal . - xsd:integer rdfs:subClassOf bsfs:Literal . + bsfs:Number rdfs:subClassOf bsfs:Literal . + xsd:integer rdfs:subClassOf bsfs:Number . # predicates mandated by Nodes bsm:t_created rdfs:subClassOf bsfs:Predicate ; diff --git a/test/graph/test_graph.py b/test/graph/test_graph.py index 125084c..f97783b 100644 --- a/test/graph/test_graph.py +++ b/test/graph/test_graph.py @@ -125,7 +125,8 @@ class TestGraph(unittest.TestCase): prefix bse: bsfs:Entity rdfs:subClassOf bsfs:Node . xsd:string rdfs:subClassOf bsfs:Literal . - xsd:integer rdfs:subClassOf bsfs:Literal . + bsfs:Number rdfs:subClassOf bsfs:Literal . + xsd:integer rdfs:subClassOf bsfs:Number . bse:filename rdfs:subClassOf bsfs:Predicate ; rdfs:domain bsfs:Entity ; @@ -147,7 +148,8 @@ class TestGraph(unittest.TestCase): prefix xsd: prefix bsfs: prefix bsm: - xsd:integer rdfs:subClassOf bsfs:Literal . + bsfs:Number rdfs:subClassOf bsfs:Literal . + xsd:integer rdfs:subClassOf bsfs:Number . bsm:t_created rdfs:subClassOf bsfs:Predicate ; rdfs:domain bsfs:Node ; rdfs:range xsd:integer ; @@ -162,7 +164,8 @@ class TestGraph(unittest.TestCase): prefix bse: bsfs:Entity rdfs:subClassOf bsfs:Node . xsd:string rdfs:subClassOf bsfs:Literal . - xsd:integer rdfs:subClassOf bsfs:Literal . + bsfs:Number rdfs:subClassOf bsfs:Literal . + xsd:integer rdfs:subClassOf bsfs:Number . bse:filename rdfs:subClassOf bsfs:Predicate ; rdfs:domain bsfs:Entity ; @@ -186,7 +189,8 @@ class TestGraph(unittest.TestCase): prefix xsd: prefix bsfs: prefix bsm: - xsd:integer rdfs:subClassOf bsfs:Literal . + bsfs:Number rdfs:subClassOf bsfs:Literal . + xsd:integer rdfs:subClassOf bsfs:Number . bsm:t_created rdfs:subClassOf bsfs:Predicate ; rdfs:domain bsfs:Node ; rdfs:range xsd:integer ; diff --git a/test/graph/test_nodes.py b/test/graph/test_nodes.py index 47647bd..e29ab6a 100644 --- a/test/graph/test_nodes.py +++ b/test/graph/test_nodes.py @@ -37,7 +37,8 @@ class TestNodes(unittest.TestCase): bsfs:Tag rdfs:subClassOf bsfs:Node . bsfs:User rdfs:subClassOf bsfs:Node . xsd:string rdfs:subClassOf bsfs:Literal . - xsd:integer rdfs:subClassOf bsfs:Literal . + bsfs:Number rdfs:subClassOf bsfs:Literal . + xsd:integer rdfs:subClassOf bsfs:Number . # predicates mandated by Nodes bsm:t_created rdfs:subClassOf bsfs:Predicate ; @@ -78,7 +79,8 @@ class TestNodes(unittest.TestCase): (rdflib.URIRef(ns.bsfs.Tag), rdflib.RDFS.subClassOf, rdflib.URIRef(ns.bsfs.Node)), (rdflib.URIRef(ns.bsfs.User), rdflib.RDFS.subClassOf, rdflib.URIRef(ns.bsfs.Node)), (rdflib.URIRef(ns.xsd.string), rdflib.RDFS.subClassOf, rdflib.URIRef(ns.bsfs.Literal)), - (rdflib.URIRef(ns.xsd.integer), rdflib.RDFS.subClassOf, rdflib.URIRef(ns.bsfs.Literal)), + (rdflib.URIRef(ns.bsfs.Number), rdflib.RDFS.subClassOf, rdflib.URIRef(ns.bsfs.Literal)), + (rdflib.URIRef(ns.xsd.integer), rdflib.RDFS.subClassOf, rdflib.URIRef(ns.bsfs.Number)), (rdflib.URIRef(ns.bsm.t_created), rdflib.RDFS.subClassOf, rdflib.URIRef(ns.bsfs.Predicate)), (rdflib.URIRef(ns.bse.comment), rdflib.RDFS.subClassOf, rdflib.URIRef(ns.bsfs.Predicate)), (rdflib.URIRef(ns.bse.filesize), rdflib.RDFS.subClassOf, rdflib.URIRef(ns.bsfs.Predicate)), diff --git a/test/graph/test_resolve.py b/test/graph/test_resolve.py index a27e8c9..0e7da99 100644 --- a/test/graph/test_resolve.py +++ b/test/graph/test_resolve.py @@ -41,7 +41,8 @@ class TestFilter(unittest.TestCase): bsfs:Entity rdfs:subClassOf bsfs:Node . bsfs:Tag rdfs:subClassOf bsfs:Node . xsd:string rdfs:subClassOf bsfs:Literal . - xsd:integer rdfs:subClassOf bsfs:Literal . + bsfs:Number rdfs:subClassOf bsfs:Literal . + xsd:integer rdfs:subClassOf bsfs:Number . bse:comment rdfs:subClassOf bsfs:Predicate ; rdfs:domain bsfs:Entity ; diff --git a/test/query/test_validator.py b/test/query/test_validator.py index 405872c..ea56a57 100644 --- a/test/query/test_validator.py +++ b/test/query/test_validator.py @@ -33,7 +33,8 @@ class TestFilter(unittest.TestCase): bsfs:Tag rdfs:subClassOf bsfs:Node . xsd:string rdfs:subClassOf bsfs:Literal . - xsd:integer rdfs:subClassOf bsfs:Literal . + bsfs:Number rdfs:subClassOf bsfs:Literal . + xsd:integer rdfs:subClassOf bsfs:Number . bse:comment rdfs:subClassOf bsfs:Predicate ; rdfs:domain bsfs:Node ; @@ -247,6 +248,9 @@ class TestFilter(unittest.TestCase): ast.filter.GreaterThan(0)) self.assertRaises(errors.ConsistencyError, self.validate._bounded, self.schema.literal(ns.bsfs.Literal).child(ns.bsfs.Invalid), ast.filter.LessThan(0)) + # type must be a number + self.assertRaises(errors.ConsistencyError, self.validate._bounded, self.schema.literal(ns.xsd.string), + ast.filter.LessThan(0)) # bounded accepts correct expressions self.assertIsNone(self.validate._bounded(self.schema.literal(ns.xsd.integer), ast.filter.LessThan(0))) self.assertIsNone(self.validate._bounded(self.schema.literal(ns.xsd.integer), ast.filter.GreaterThan(0))) diff --git a/test/schema/test_schema.py b/test/schema/test_schema.py index ca21f87..c19c226 100644 --- a/test/schema/test_schema.py +++ b/test/schema/test_schema.py @@ -35,7 +35,8 @@ class TestSchema(unittest.TestCase): bsfs:Unused rdfs:subClassOf bsfs:Node . xsd:string rdfs:subClassOf bsfs:Literal . - xsd:integer rdfs:subClassOf bsfs:Literal . + bsfs:Number rdfs:subClassOf bsfs:Literal . + xsd:integer rdfs:subClassOf bsfs:Number . xsd:boolean rdfs:subClassOf bsfs:Literal . bse:tag rdfs:subClassOf bsfs:Predicate ; @@ -56,18 +57,18 @@ class TestSchema(unittest.TestCase): ''' # nodes self.n_root = types.ROOT_NODE - self.n_ent = types.Node(ns.bsfs.Entity, types.Node(ns.bsfs.Node, None)) - self.n_img = types.Node(ns.bsfs.Image, types.Node(ns.bsfs.Entity, types.Node(ns.bsfs.Node, None))) - self.n_tag = types.Node(ns.bsfs.Tag, types.Node(ns.bsfs.Node, None)) - self.n_unused = types.Node(ns.bsfs.Unused, types.Node(ns.bsfs.Node, None)) + self.n_ent = self.n_root.child(ns.bsfs.Entity) + self.n_img = self.n_ent.child(ns.bsfs.Image) + self.n_tag = self.n_root.child(ns.bsfs.Tag) + self.n_unused = self.n_root.child(ns.bsfs.Unused) self.nodes = [self.n_root, self.n_ent, self.n_img, self.n_tag, self.n_unused] # literals self.l_root = types.ROOT_LITERAL self.l_number = types.ROOT_NUMBER - self.l_string = types.Literal(ns.xsd.string, types.Literal(ns.bsfs.Literal, None)) - self.l_integer = types.Literal(ns.xsd.integer, self.l_number) - self.l_unused = types.Literal(ns.xsd.boolean, types.Literal(ns.bsfs.Literal, None)) + self.l_string = self.l_root.child(ns.xsd.string) + self.l_integer = self.l_root.child(ns.xsd.integer) + self.l_unused = self.l_root.child(ns.xsd.boolean) self.literals = [self.l_root, self.l_number, self.l_string, self.l_integer, self.l_unused] # predicates diff --git a/test/schema/test_serialize.py b/test/schema/test_serialize.py index b9d8599..f46b3a4 100644 --- a/test/schema/test_serialize.py +++ b/test/schema/test_serialize.py @@ -180,7 +180,7 @@ class TestFromString(unittest.TestCase): # a literal can have multiple children l_string = types.ROOT_LITERAL.child(ns.xsd.string) - l_integer = types.ROOT_LITERAL.child(ns.xsd.integer) + l_integer = types.ROOT_NUMBER.child(ns.xsd.integer) l_unsigned = l_integer.child(ns.xsd.unsigned) l_signed = l_integer.child(ns.xsd.signed) self.assertEqual(Schema({}, {}, {l_string, l_integer, l_unsigned, l_signed}), from_string(''' @@ -191,7 +191,8 @@ class TestFromString(unittest.TestCase): # literals inherit from same parent xsd:string rdfs:subClassOf bsfs:Literal . - xsd:integer rdfs:subClassOf bsfs:Literal . + bsfs:Number rdfs:subClassOf bsfs:Literal . + xsd:integer rdfs:subClassOf bsfs:Number . # literals inherit from same parent xsd:unsigned rdfs:subClassOf xsd:integer . @@ -893,7 +894,7 @@ class TestFromString(unittest.TestCase): # literals l_string = types.ROOT_LITERAL.child(ns.xsd.string) l_array = types.ROOT_LITERAL.child(ns.bsfs.array) - l_integer = types.ROOT_LITERAL.child(ns.xsd.integer) + l_integer = types.ROOT_NUMBER.child(ns.xsd.integer) l_boolean = types.ROOT_LITERAL.child(ns.xsd.boolean) # predicates p_annotation = types.ROOT_PREDICATE.child(ns.bsfs.Annotation) @@ -931,7 +932,8 @@ class TestFromString(unittest.TestCase): xsd:string rdfs:subClassOf bsfs:Literal ; rdfs:label "A sequence of characters"^^xsd:string . bsfs:array rdfs:subClassOf bsfs:Literal . - xsd:integer rdfs:subClassOf bsfs:Literal . + bsfs:Number rdfs:subClassOf bsfs:Literal . + xsd:integer rdfs:subClassOf bsfs:Number . xsd:boolean rdfs:subClassOf bsfs:Literal . # abstract predicates diff --git a/test/triple_store/sparql/test_parse_filter.py b/test/triple_store/sparql/test_parse_filter.py index 1d96994..f6842c5 100644 --- a/test/triple_store/sparql/test_parse_filter.py +++ b/test/triple_store/sparql/test_parse_filter.py @@ -35,7 +35,8 @@ class TestParseFilter(unittest.TestCase): bsfs:Tag rdfs:subClassOf bsfs:Node . xsd:string rdfs:subClassOf bsfs:Literal . - xsd:integer rdfs:subClassOf bsfs:Literal . + bsfs:Number rdfs:subClassOf bsfs:Literal . + xsd:integer rdfs:subClassOf bsfs:Number . bsfs:URI rdfs:subClassOf bsfs:Literal . bse:comment rdfs:subClassOf bsfs:Predicate ; diff --git a/test/triple_store/sparql/test_sparql.py b/test/triple_store/sparql/test_sparql.py index 5342925..5b71016 100644 --- a/test/triple_store/sparql/test_sparql.py +++ b/test/triple_store/sparql/test_sparql.py @@ -33,7 +33,8 @@ class TestSparqlStore(unittest.TestCase): bsfs:Tag rdfs:subClassOf bsfs:Node . bsfs:User rdfs:subClassOf bsfs:Node . xsd:string rdfs:subClassOf bsfs:Literal . - xsd:integer rdfs:subClassOf bsfs:Literal . + bsfs:Number rdfs:subClassOf bsfs:Literal . + xsd:integer rdfs:subClassOf bsfs:Number . # non-unique literal bse:comment rdfs:subClassOf bsfs:Predicate ; @@ -66,7 +67,8 @@ class TestSparqlStore(unittest.TestCase): (rdflib.URIRef(ns.bsfs.Tag), rdflib.RDFS.subClassOf, rdflib.URIRef(ns.bsfs.Node)), (rdflib.URIRef(ns.bsfs.User), rdflib.RDFS.subClassOf, rdflib.URIRef(ns.bsfs.Node)), (rdflib.URIRef(ns.xsd.string), rdflib.RDFS.subClassOf, rdflib.URIRef(ns.bsfs.Literal)), - (rdflib.URIRef(ns.xsd.integer), rdflib.RDFS.subClassOf, rdflib.URIRef(ns.bsfs.Literal)), + (rdflib.URIRef(ns.bsfs.Number), rdflib.RDFS.subClassOf, rdflib.URIRef(ns.bsfs.Literal)), + (rdflib.URIRef(ns.xsd.integer), rdflib.RDFS.subClassOf, rdflib.URIRef(ns.bsfs.Number)), (rdflib.URIRef(ns.bse.comment), rdflib.RDFS.subClassOf, rdflib.URIRef(ns.bsfs.Predicate)), (rdflib.URIRef(ns.bse.filesize), rdflib.RDFS.subClassOf, rdflib.URIRef(ns.bsfs.Predicate)), (rdflib.URIRef(ns.bse.tag), rdflib.RDFS.subClassOf, rdflib.URIRef(ns.bsfs.Predicate)), @@ -310,7 +312,8 @@ class TestSparqlStore(unittest.TestCase): bsfs:User rdfs:subClassOf bsfs:Node . xsd:boolean rdfs:subClassOf bsfs:Literal . - xsd:integer rdfs:subClassOf bsfs:Literal . + bsfs:Number rdfs:subClassOf bsfs:Literal . + xsd:integer rdfs:subClassOf bsfs:Number . bse:filesize rdfs:subClassOf bsfs:Predicate ; rdfs:domain bsfs:Entity ; @@ -351,7 +354,8 @@ class TestSparqlStore(unittest.TestCase): (rdflib.URIRef(ns.bsfs.Tag), rdflib.RDFS.subClassOf, rdflib.URIRef(ns.bsfs.Node)), (rdflib.URIRef(ns.bsfs.User), rdflib.RDFS.subClassOf, rdflib.URIRef(ns.bsfs.Node)), (rdflib.URIRef(ns.xsd.boolean), rdflib.RDFS.subClassOf, rdflib.URIRef(ns.bsfs.Literal)), - (rdflib.URIRef(ns.xsd.integer), rdflib.RDFS.subClassOf, rdflib.URIRef(ns.bsfs.Literal)), + (rdflib.URIRef(ns.bsfs.Number), rdflib.RDFS.subClassOf, rdflib.URIRef(ns.bsfs.Literal)), + (rdflib.URIRef(ns.xsd.integer), rdflib.RDFS.subClassOf, rdflib.URIRef(ns.bsfs.Number)), (rdflib.URIRef(ns.bse.shared), rdflib.RDFS.subClassOf, rdflib.URIRef(ns.bsfs.Predicate)), (rdflib.URIRef(ns.bse.tag), rdflib.RDFS.subClassOf, rdflib.URIRef(ns.bsfs.Predicate)), (rdflib.URIRef(ns.bse.filesize), rdflib.RDFS.subClassOf, rdflib.URIRef(ns.bsfs.Predicate)), -- cgit v1.2.3 From e708016ae366e96051281f3a744af35a8c06d98b Mon Sep 17 00:00:00 2001 From: Matthias Baumgartner Date: Thu, 12 Jan 2023 10:28:16 +0100 Subject: cleanup and cosmetic changes --- bsfs/schema/__init__.py | 3 ++- bsfs/schema/schema.py | 2 -- bsfs/schema/serialize.py | 17 +++++++++-------- bsfs/schema/types.py | 8 ++++---- bsfs/triple_store/sparql/sparql.py | 6 +++--- test/graph/test_nodes.py | 1 + test/graph/test_resolve.py | 2 +- test/triple_store/sparql/test_sparql.py | 2 ++ 8 files changed, 22 insertions(+), 19 deletions(-) diff --git a/bsfs/schema/__init__.py b/bsfs/schema/__init__.py index 5162a01..31d7d61 100644 --- a/bsfs/schema/__init__.py +++ b/bsfs/schema/__init__.py @@ -10,7 +10,8 @@ import typing # inner-module imports from .schema import Schema from .serialize import from_string, to_string -from .types import Literal, Node, Predicate, Vertex, ROOT_FEATURE, ROOT_LITERAL, ROOT_NODE, ROOT_NUMBER, ROOT_PREDICATE, ROOT_VERTEX +from .types import Literal, Node, Predicate, Vertex, \ + ROOT_FEATURE, ROOT_LITERAL, ROOT_NODE, ROOT_NUMBER, ROOT_PREDICATE, ROOT_VERTEX # exports __all__: typing.Sequence[str] = ( diff --git a/bsfs/schema/schema.py b/bsfs/schema/schema.py index 80cb58a..52ad191 100644 --- a/bsfs/schema/schema.py +++ b/bsfs/schema/schema.py @@ -7,10 +7,8 @@ Author: Matthias Baumgartner, 2022 # imports from collections import abc, namedtuple import typing -import rdflib # bsfs imports -from bsfs.namespace import ns from bsfs.utils import errors, URI, typename # inner-module imports diff --git a/bsfs/schema/serialize.py b/bsfs/schema/serialize.py index c1ac9a9..0eb6628 100644 --- a/bsfs/schema/serialize.py +++ b/bsfs/schema/serialize.py @@ -5,7 +5,6 @@ A copy of the license is provided with the project. Author: Matthias Baumgartner, 2022 """ # standard imports -from collections import abc import itertools import typing @@ -42,7 +41,7 @@ def from_string(schema_str: str) -> schema.Schema: return value.value if isinstance(value, rdflib.URIRef): return URI(value) - raise errors.BackendError(f'expected Literal or URIRef, found {typename(value)}') + raise errors.UnreachableError(f'expected Literal or URIRef, found {typename(value)}') def _fetch_hierarchically(factory, curr): """Walk through a rdfs:subClassOf hierarchy, creating symbols along the way.""" @@ -80,14 +79,16 @@ def from_string(schema_str: str) -> schema.Schema: # fetch predicates # FIXME: type annotation def _fetch_value(subject: URI, predicate: rdflib.URIRef, value_factory) -> typing.Optional[typing.Any]: - """Fetch the object of a given subject and predicate. Raises a `errors.ConsistencyError` if multiple objects match.""" + """Fetch the object of a given subject and predicate. + Raises a `errors.ConsistencyError` if multiple objects match. + """ values = list(graph.objects(rdflib.URIRef(subject), predicate)) if len(values) == 0: return None - elif len(values) == 1: + if len(values) == 1: return value_factory(values[0]) - else: - raise errors.ConsistencyError(f'{subject} has multiple values for predicate {str(predicate)}, expected zero or one') + raise errors.ConsistencyError( + f'{subject} has multiple values for predicate {str(predicate)}, expected zero or one') def _build_predicate(uri, parent, **annotations): """Predicate factory.""" @@ -102,13 +103,13 @@ def from_string(schema_str: str) -> schema.Schema: dom = _fetch_value(uri, rdflib.RDFS.domain, URI) if dom is not None and dom not in nodes_lut: raise errors.ConsistencyError(f'predicate {uri} has undefined domain {dom}') - elif dom is not None: + if dom is not None: dom = nodes_lut[dom] # get range rng = _fetch_value(uri, rdflib.RDFS.range, URI) if rng is not None and rng not in nodes_lut and rng not in literals_lut: raise errors.ConsistencyError(f'predicate {uri} has undefined range {rng}') - elif rng is not None: + if rng is not None: rng = nodes_lut.get(rng, literals_lut.get(rng)) # get unique unique = _fetch_value(uri, rdflib.URIRef(ns.bsfs.unique), bool) diff --git a/bsfs/schema/types.py b/bsfs/schema/types.py index 4f49efe..6257dee 100644 --- a/bsfs/schema/types.py +++ b/bsfs/schema/types.py @@ -238,7 +238,7 @@ class Predicate(_Type): self, # Type members uri: URI, - parent: '_PredicateBase', + parent: typing.Optional['Predicate'], # Predicate members domain: Node, range: Vertex, # pylint: disable=redefined-builtin @@ -312,10 +312,10 @@ class Feature(Predicate): self, # Type members uri: URI, - parent: Predicate, + parent: typing.Optional[Predicate], # Predicate members domain: Node, - range: Literal, + range: Literal, # pylint: disable=redefined-builtin unique: bool, # Feature members dimension: int, @@ -341,7 +341,7 @@ class Feature(Predicate): self, uri: URI, domain: typing.Optional[Node] = None, - range: typing.Optional[Literal] = None, # pylint: disable=redefined-builtin + range: typing.Optional[Vertex] = None, # pylint: disable=redefined-builtin unique: typing.Optional[bool] = None, dimension: typing.Optional[int] = None, dtype: typing.Optional[URI] = None, diff --git a/bsfs/triple_store/sparql/sparql.py b/bsfs/triple_store/sparql/sparql.py index 87467ff..3877d1a 100644 --- a/bsfs/triple_store/sparql/sparql.py +++ b/bsfs/triple_store/sparql/sparql.py @@ -139,7 +139,7 @@ class SparqlStore(base.TripleStoreBase): for src, trg in self._graph.subject_objects(rdflib.URIRef(pred.uri)): self._transaction.remove((src, rdflib.URIRef(pred.uri), trg)) # remove predicate definition - if pred.parent is not None: + if pred.parent is not None: # NOTE: there shouldn't be any predicate w/o parent self._transaction.remove(( rdflib.URIRef(pred.uri), rdflib.RDFS.subClassOf, @@ -159,7 +159,7 @@ class SparqlStore(base.TripleStoreBase): # remove instance self._transaction.remove((inst, rdflib.RDF.type, rdflib.URIRef(node.uri))) # remove node definition - if node.parent is not None: + if node.parent is not None: # NOTE: there shouldn't be any node w/o parent self._transaction.remove(( rdflib.URIRef(node.uri), rdflib.RDFS.subClassOf, @@ -168,7 +168,7 @@ class SparqlStore(base.TripleStoreBase): for lit in sub.literals: # remove literal definition - if lit.parent is not None: + if lit.parent is not None: # NOTE: there shouldn't be any literal w/o parent self._transaction.remove(( rdflib.URIRef(lit.uri), rdflib.RDFS.subClassOf, diff --git a/test/graph/test_nodes.py b/test/graph/test_nodes.py index e29ab6a..81da60f 100644 --- a/test/graph/test_nodes.py +++ b/test/graph/test_nodes.py @@ -75,6 +75,7 @@ class TestNodes(unittest.TestCase): ''') self.schema_triples = { # schema hierarchy + (rdflib.URIRef(ns.bsfs.Feature), rdflib.RDFS.subClassOf, rdflib.URIRef(ns.bsfs.Predicate)), (rdflib.URIRef(ns.bsfs.Entity), rdflib.RDFS.subClassOf, rdflib.URIRef(ns.bsfs.Node)), (rdflib.URIRef(ns.bsfs.Tag), rdflib.RDFS.subClassOf, rdflib.URIRef(ns.bsfs.Node)), (rdflib.URIRef(ns.bsfs.User), rdflib.RDFS.subClassOf, rdflib.URIRef(ns.bsfs.Node)), diff --git a/test/graph/test_resolve.py b/test/graph/test_resolve.py index 0e7da99..9cde38e 100644 --- a/test/graph/test_resolve.py +++ b/test/graph/test_resolve.py @@ -150,7 +150,7 @@ class TestFilter(unittest.TestCase): ast.filter.Predicate(ns.bse.comment)) self.assertRaises(errors.BackendError, resolver, schema.node(ns.bsfs.Tag), ast.filter.Any(ast.filter.PredicateExpression(), ast.filter.Equals('foo'))) - self.assertRaises(errors.UnreachableError, resolver._one_of, ast.filter.OneOf(ast.filter.Predicate(ns.bsfs.Predicate))) + self.assertRaises(errors.BackendError, resolver._one_of, ast.filter.OneOf(ast.filter.Predicate(ns.bsfs.Predicate))) # check schema consistency self.assertRaises(errors.ConsistencyError, resolver, schema.node(ns.bsfs.Tag), diff --git a/test/triple_store/sparql/test_sparql.py b/test/triple_store/sparql/test_sparql.py index 5b71016..aa5dfc7 100644 --- a/test/triple_store/sparql/test_sparql.py +++ b/test/triple_store/sparql/test_sparql.py @@ -63,6 +63,7 @@ class TestSparqlStore(unittest.TestCase): ''') self.schema_triples = { # schema hierarchy + (rdflib.URIRef(ns.bsfs.Feature), rdflib.RDFS.subClassOf, rdflib.URIRef(ns.bsfs.Predicate)), (rdflib.URIRef(ns.bsfs.Entity), rdflib.RDFS.subClassOf, rdflib.URIRef(ns.bsfs.Node)), (rdflib.URIRef(ns.bsfs.Tag), rdflib.RDFS.subClassOf, rdflib.URIRef(ns.bsfs.Node)), (rdflib.URIRef(ns.bsfs.User), rdflib.RDFS.subClassOf, rdflib.URIRef(ns.bsfs.Node)), @@ -350,6 +351,7 @@ class TestSparqlStore(unittest.TestCase): # instances of old classes were removed self.assertSetEqual(set(store._graph), { # schema hierarchy + (rdflib.URIRef(ns.bsfs.Feature), rdflib.RDFS.subClassOf, rdflib.URIRef(ns.bsfs.Predicate)), (rdflib.URIRef(ns.bsfs.Entity), rdflib.RDFS.subClassOf, rdflib.URIRef(ns.bsfs.Node)), (rdflib.URIRef(ns.bsfs.Tag), rdflib.RDFS.subClassOf, rdflib.URIRef(ns.bsfs.Node)), (rdflib.URIRef(ns.bsfs.User), rdflib.RDFS.subClassOf, rdflib.URIRef(ns.bsfs.Node)), -- cgit v1.2.3 From 1b7ef16c3795bb7112683662b8c22a774e219269 Mon Sep 17 00:00:00 2001 From: Matthias Baumgartner Date: Thu, 12 Jan 2023 16:57:58 +0100 Subject: schema to string --- bsfs/schema/schema.py | 2 + bsfs/schema/serialize.py | 104 ++++++++++++++++++++++++- test/schema/test_serialize.py | 173 +++++++++++++++++++++++++++++++++++++++++- 3 files changed, 274 insertions(+), 5 deletions(-) diff --git a/bsfs/schema/schema.py b/bsfs/schema/schema.py index 52ad191..bc50d4e 100644 --- a/bsfs/schema/schema.py +++ b/bsfs/schema/schema.py @@ -72,6 +72,8 @@ class Schema(): literals.add(types.ROOT_NUMBER) predicates.add(types.ROOT_FEATURE) + # FIXME: ensure that types derive from the right root? + # include parents in predicates set # TODO: review type annotations and ignores for python >= 3.11 (parents is _Type but should be typing.Self) predicates |= {par for pred in predicates for par in pred.parents()} # type: ignore [misc] diff --git a/bsfs/schema/serialize.py b/bsfs/schema/serialize.py index 0eb6628..a566d65 100644 --- a/bsfs/schema/serialize.py +++ b/bsfs/schema/serialize.py @@ -136,9 +136,107 @@ def from_string(schema_str: str) -> schema.Schema: -def to_string(schema_inst: schema.Schema) -> str: +def to_string(schema_inst: schema.Schema, fmt: str = 'turtle') -> str: + """Serialize a `bsfs.schema.Schema` to a string. + See `rdflib.Graph.serialize` for viable formats (default: turtle). """ - """ - raise NotImplementedError() + + # type of emitted triples. + T_TRIPLE = typing.Iterator[typing.Tuple[rdflib.URIRef, rdflib.URIRef, rdflib.term.Identifier]] + + def _type(tpe: types._Type) -> T_TRIPLE : + """Emit _Type properties (parent, annotations).""" + # emit parent + if tpe.parent is not None: + yield ( + rdflib.URIRef(tpe.uri), + rdflib.URIRef(ns.rdfs.subClassOf), + rdflib.URIRef(tpe.parent.uri), + ) + # emit annotations + for prop, value in tpe.annotations.items(): + yield ( + rdflib.URIRef(tpe.uri), + rdflib.URIRef(prop), + rdflib.Literal(value), # FIXME: datatype?! + ) + + def _predicate(pred: types.Predicate) -> T_TRIPLE: + """Emit Predicate properties (domain, range, unique).""" + # no need to emit anything for the root predicate + if pred == types.ROOT_PREDICATE: + return + # emit domain + if pred.domain != getattr(pred.parent, 'domain', None): + yield ( + rdflib.URIRef(pred.uri), + rdflib.URIRef(ns.rdfs.domain), + rdflib.URIRef(pred.domain.uri), + ) + # emit range + if pred.range != getattr(pred.parent, 'range', None): + yield ( + rdflib.URIRef(pred.uri), + rdflib.URIRef(ns.rdfs.range), + rdflib.URIRef(pred.range.uri), + ) + # emit cardinality + if pred.unique != getattr(pred.parent, 'unique', None): + yield ( + rdflib.URIRef(pred.uri), + rdflib.URIRef(ns.bsfs.unique), + rdflib.Literal(pred.unique, datatype=rdflib.XSD.boolean), + ) + + def _feature(feat: types.Feature) -> T_TRIPLE: + """Emit Feature properties (dimension, dtype, distance).""" + # emit size + if feat.dimension != getattr(feat.parent, 'dimension', None): + yield ( + rdflib.URIRef(feat.uri), + rdflib.URIRef(ns.bsfs.dimension), + rdflib.Literal(feat.dimension, datatype=rdflib.XSD.integer), + ) + # emit dtype + if feat.dtype != getattr(feat.parent, 'dtype', None): + yield ( + rdflib.URIRef(feat.uri), + rdflib.URIRef(ns.bsfs.dtype), + rdflib.URIRef(feat.dtype), + ) + # emit distance + if feat.distance != getattr(feat.parent, 'distance', None): + yield ( + rdflib.URIRef(feat.uri), + rdflib.URIRef(ns.bsfs.distance), + rdflib.URIRef(feat.distance), + ) + + def _parse(node: types._Type) -> T_TRIPLE: + """Emit all properties of a type.""" + if isinstance(node, types._Type): # pylint: disable=protected-access + # NOTE: all nodes are _Type + yield from _type(node) + if isinstance(node, types.Predicate): + yield from _predicate(node) + if isinstance(node, types.Feature): + yield from _feature(node) + + # create graph + graph = rdflib.Graph() + # add triples to graph + nodes = itertools.chain( + schema_inst.nodes(), + schema_inst.literals(), + schema_inst.predicates()) + for node in nodes: + for triple in _parse(node): + graph.add(triple) + # add known namespaces for readability + # FIXME: more systematically (e.g. for all in ns?) + graph.bind('bsfs', rdflib.URIRef('http://bsfs.ai/schema/')) + graph.bind('bse', rdflib.URIRef('http://bsfs.ai/schema/Entity#')) + # serialize to turtle + return graph.serialize(format=fmt) ## EOF ## diff --git a/test/schema/test_serialize.py b/test/schema/test_serialize.py index f46b3a4..205150a 100644 --- a/test/schema/test_serialize.py +++ b/test/schema/test_serialize.py @@ -5,6 +5,7 @@ A copy of the license is provided with the project. Author: Matthias Baumgartner, 2022 """ # imports +import re import unittest # bsfs imports @@ -997,8 +998,176 @@ class TestFromString(unittest.TestCase): class TestToString(unittest.TestCase): - def test_stub(self): - raise NotImplementedError() + + def test_empty(self): + self.assertEqual(Schema(), from_string(to_string(Schema()))) + + def test_literal(self): + # root literals + l_str = types.ROOT_LITERAL.child(ns.xsd.string) + # derived literals + l_int = types.ROOT_NUMBER.child(ns.xsd.integer) + l_unsigned = l_int.child(ns.xsd.unsigned) + # create schema + schema = Schema(literals={l_int, l_str, l_unsigned}) + + schema_str = to_string(schema) + # all symbols are serialized + self.assertIn('xsd:string', schema_str) + self.assertIn('xsd:integer', schema_str) + self.assertIn('xsd:unsigned', schema_str) + # unserialize yields the original schema + self.assertEqual(schema, from_string(schema_str)) + + # literals that have no parent are ignored + schema = Schema(literals={types.Literal(ns.bsfs.Invalid, None)}) + self.assertEqual(Schema(), from_string(to_string(schema))) + self.assertNotIn('Invalid', to_string(schema)) + + # literal annotations are serialized + annotations = { + ns.rdfs.label: 'hello world', + ns.schema.description: 'some text', + ns.bsfs.foo: 1234, + ns.bsfs.bar: True, + } + l_str = types.ROOT_LITERAL.child(ns.xsd.string, **annotations) + self.assertDictEqual( + annotations, + from_string(to_string(Schema(literals={l_str}))).literal(ns.xsd.string).annotations) + + + def test_node(self): + # root nodes + n_ent = types.ROOT_NODE.child(ns.bsfs.Entity) + n_tag = types.ROOT_NODE.child(ns.bsfs.Tag) + # derived nodes + n_img = n_ent.child(ns.bsfs.Image) + n_doc = n_ent.child(ns.bsfs.Document) + n_grp = n_tag.child(ns.bsfs.Group) + # create schema + schema = Schema(nodes={n_ent, n_img, n_doc, n_tag, n_grp}) + + schema_str = to_string(schema) + # all symbols are serialized + self.assertIn('bsfs:Entity', schema_str) + self.assertIn('bsfs:Tag', schema_str) + self.assertIn('bsfs:Image', schema_str) + self.assertIn('bsfs:Document', schema_str) + self.assertIn('bsfs:Group', schema_str) + # unserialize yields the original schema + self.assertEqual(schema, from_string(schema_str)) + + # nodes that have no parent are ignored + schema = Schema(nodes={types.Node(ns.bsfs.Invalid, None)}) + self.assertEqual(Schema(), from_string(to_string(schema))) + self.assertNotIn('Invalid', to_string(schema)) + + # node annotations are serialized + annotations = { + ns.rdfs.label: 'hello world', + ns.schema.description: 'some text', + ns.bsfs.foo: 1234, + ns.bsfs.bar: True, + } + n_ent = types.ROOT_NODE.child(ns.bsfs.Entity, **annotations) + self.assertDictEqual( + annotations, + from_string(to_string(Schema(nodes={n_ent}))).node(ns.bsfs.Entity).annotations) + + + def test_predicate(self): + # auxiliary types + n_ent = types.ROOT_NODE.child(ns.bsfs.Entity) + l_str = types.ROOT_LITERAL.child(ns.xsd.string) + # root predicates + p_annotation = types.ROOT_PREDICATE.child(ns.bsfs.Annotation, domain=n_ent) + p_owner = types.ROOT_PREDICATE.child(ns.bse.owner, range=l_str, unique=True) + # derived predicates + p_comment = p_annotation.child(ns.bse.comment, range=l_str) # inherits domain + p_note = p_comment.child(ns.bse.note, unique=True) # inherits domain/range + # create schema + schema = Schema({p_owner, p_comment, p_note}) + + schema_str = to_string(schema) + # all symbols are serialized + self.assertIn('bsfs:Entity', schema_str) + self.assertIn('xsd:string', schema_str) + self.assertIn('bsfs:Annotation', schema_str) + self.assertIn('bse:comment', schema_str) + self.assertIn('bse:owner', schema_str) + self.assertIn('bse:note', schema_str) + # inherited properties are not serialized + self.assertIsNotNone(re.search(r'bse:comment[^\.]*rdfs:range[^\.]', schema_str)) + self.assertIsNone(re.search(r'bse:comment[^\.]*rdfs:domain[^\.]', schema_str)) + #p_note has no domain/range + self.assertIsNone(re.search(r'bse:note[^\.]*rdfs:domain[^\.]', schema_str)) + self.assertIsNone(re.search(r'bse:note[^\.]*rdfs:range[^\.]', schema_str)) + # unserialize yields the original schema + self.assertEqual(schema, from_string(schema_str)) + + # predicate annotations are serialized + annotations = { + ns.rdfs.label: 'hello world', + ns.schema.description: 'some text', + ns.bsfs.foo: 1234, + ns.bsfs.bar: False, + } + p_annotation = types.ROOT_PREDICATE.child(ns.bsfs.Annotation, **annotations) + self.assertDictEqual( + annotations, + from_string(to_string(Schema({p_annotation}))).predicate(ns.bsfs.Annotation).annotations) + + + def test_feature(self): + # auxiliary types + n_ent = types.ROOT_NODE.child(ns.bsfs.Entity) + l_array = types.ROOT_LITERAL.child(ns.bsfs.array) + # root features + f_colors = types.ROOT_FEATURE.child(URI('http://bsfs.ai/schema/Feature/colors'), + range=l_array, unique=True, distance=ns.bsfs.cosine) + # derived features + f_colors1234 = f_colors.child(URI('http://bsfs.ai/schema/Feature/colors#1234'), + dimension=1024, domain=n_ent) # inherits range/dtype/distance + f_colors4321 = f_colors.child(URI('http://bsfs.ai/schema/Feature/colors#4321'), + dimension=2048, distance=ns.bsfs.euclidean) # inherits domain/range/dtype + # create schema + schema = Schema({f_colors, f_colors1234, f_colors4321}) + + schema_str = to_string(schema) + # all symbols are serialized + self.assertIn('bsfs:Entity', schema_str) + self.assertIn('bsfs:array', schema_str) + self.assertIn('[^\.]*rdfs:domain[^\.]', schema_str)) + self.assertIsNotNone(re.search(r'[^\.]*bsfs:dimension[^\.]', schema_str)) + self.assertIsNone(re.search(r'[^\.]*rdfs:range[^\.]', schema_str)) + self.assertIsNone(re.search(r'[^\.]*bsfs:dtype[^\.]', schema_str)) + self.assertIsNone(re.search(r'[^\.]*bsfs:distance[^\.]', schema_str)) + self.assertIsNotNone(re.search(r'[^\.]*bsfs:dimension[^\.]', schema_str)) + self.assertIsNotNone(re.search(r'[^\.]*bsfs:distance[^\.]', schema_str)) + self.assertIsNone(re.search(r'[^\.]*rdfs:domain[^\.]', schema_str)) + self.assertIsNone(re.search(r'[^\.]*rdfs:range[^\.]', schema_str)) + self.assertIsNone(re.search(r'[^\.]*bsfs:dtype[^\.]', schema_str)) + # unserialize yields the original schema + self.assertEqual(schema, from_string(schema_str)) + + # predicate annotations are serialized + annotations = { + ns.rdfs.label: 'hello world', + ns.schema.description: 'some text', + ns.bsfs.foo: 1234, + ns.bsfs.bar: False, + } + f_colors = types.ROOT_FEATURE.child(URI('http://bsfs.ai/schema/Feature/colors'), + domain=n_ent, range=l_array, unique=True, dtype=ns.bsfs.f16, distance=ns.bsfs.euclidean, + **annotations) + self.assertDictEqual( + annotations, + from_string(to_string(Schema({f_colors}))).predicate(URI('http://bsfs.ai/schema/Feature/colors')).annotations) ## main ## -- cgit v1.2.3 From 60257ed3c2aa6ea2891f362a691bde9d7ef17831 Mon Sep 17 00:00:00 2001 From: Matthias Baumgartner Date: Fri, 13 Jan 2023 12:22:34 +0100 Subject: schema type comparison across classes --- bsfs/graph/resolve.py | 10 +++++----- bsfs/query/validator.py | 31 ++++++++++++------------------ bsfs/schema/types.py | 16 ++++++++++++---- bsfs/triple_store/sparql/parse_filter.py | 15 ++++++--------- test/schema/test_types.py | 33 ++++++++++++++++---------------- 5 files changed, 52 insertions(+), 53 deletions(-) diff --git a/bsfs/graph/resolve.py b/bsfs/graph/resolve.py index 9b5f631..b671204 100644 --- a/bsfs/graph/resolve.py +++ b/bsfs/graph/resolve.py @@ -96,11 +96,11 @@ class Filter(): # parse child expression subrng = self._parse_predicate_expression(pred) # determine the next type - try: - if rng is None or subrng > rng: # pick most generic range - rng = subrng - except TypeError as err: - raise errors.ConsistencyError(f'ranges {subrng} and {rng} are not related') from err + if rng is None or subrng > rng: # pick most generic range + rng = subrng + # check range consistency + if not subrng <= rng and not subrng >= rng: + raise errors.ConsistencyError(f'ranges {subrng} and {rng} are not related') if not isinstance(rng, (bsc.Node, bsc.Literal)): raise errors.BackendError(f'the range of node {node} is undefined') return rng diff --git a/bsfs/query/validator.py b/bsfs/query/validator.py index 75b51ca..ecea951 100644 --- a/bsfs/query/validator.py +++ b/bsfs/query/validator.py @@ -113,25 +113,18 @@ class Filter(): for pred in node: # parse child expression subdom, subrng = self._parse_predicate_expression(pred) - try: - # determine overall domain - if dom is None or subdom < dom: # pick most specific domain - dom = subdom - # domains must be related across all child expressions - if not subdom <= dom and not subdom >= dom: - raise errors.ConsistencyError(f'domains {subdom} and {dom} are not related') - except TypeError as err: # compared literal vs. node - raise errors.ConsistencyError(f'domains {subdom} and {dom} are not of the same type') from err - - try: - # determine overall range - if rng is None or subrng > rng: # pick most generic range - rng = subrng - # ranges must be related across all child expressions - if not subrng <= rng and not subrng >= rng: - raise errors.ConsistencyError(f'ranges {subrng} and {rng} are not related') - except TypeError as err: # compared literal vs. node - raise errors.ConsistencyError(f'ranges {subrng} and {rng} are not of the same type') from err + # determine overall domain + if dom is None or subdom < dom: # pick most specific domain + dom = subdom + # domains must be related across all child expressions + if not subdom <= dom and not subdom >= dom: + raise errors.ConsistencyError(f'domains {subdom} and {dom} are not related') + # determine overall range + if rng is None or subrng > rng: # pick most generic range + rng = subrng + # ranges must be related across all child expressions + if not subrng <= rng and not subrng >= rng: + raise errors.ConsistencyError(f'ranges {subrng} and {rng} are not related') # OneOf guarantees at least one expression, dom and rng are always bsc.Vertex. # mypy does not realize this, hence we ignore the warning. return dom, rng # type: ignore [return-value] diff --git a/bsfs/schema/types.py b/bsfs/schema/types.py index 6257dee..95dc66a 100644 --- a/bsfs/schema/types.py +++ b/bsfs/schema/types.py @@ -150,8 +150,10 @@ class _Type(): def __lt__(self, other: typing.Any) -> bool: """Return True iff *self* is a true subclass of *other*.""" - if not isinstance(other, type(self)): + if not isinstance(other, _Type): return NotImplemented + if not isinstance(other, type(self)): # FIXME: necessary? + return False if self.uri == other.uri: # equivalence return False if self in other.parents(): # superclass @@ -163,8 +165,10 @@ class _Type(): def __le__(self, other: typing.Any) -> bool: """Return True iff *self* is equivalent or a subclass of *other*.""" - if not isinstance(other, type(self)): + if not isinstance(other, _Type): return NotImplemented + if not isinstance(other, type(self)): # FIXME: necessary? + return False if self.uri == other.uri: # equivalence return True if self in other.parents(): # superclass @@ -176,8 +180,10 @@ class _Type(): def __gt__(self, other: typing.Any) -> bool: """Return True iff *self* is a true superclass of *other*.""" - if not isinstance(other, type(self)): + if not isinstance(other, _Type): return NotImplemented + if not isinstance(other, type(self)): # FIXME: necessary? + return False if self.uri == other.uri: # equivalence return False if self in other.parents(): # superclass @@ -189,8 +195,10 @@ class _Type(): def __ge__(self, other: typing.Any) -> bool: """Return True iff *self* is eqiuvalent or a superclass of *other*.""" - if not isinstance(other, type(self)): + if not isinstance(other, _Type): return NotImplemented + if not isinstance(other, type(self)): # FIXME: necessary? + return False if self.uri == other.uri: # equivalence return True if self in other.parents(): # superclass diff --git a/bsfs/triple_store/sparql/parse_filter.py b/bsfs/triple_store/sparql/parse_filter.py index 18a3288..5d8a2d9 100644 --- a/bsfs/triple_store/sparql/parse_filter.py +++ b/bsfs/triple_store/sparql/parse_filter.py @@ -131,15 +131,12 @@ class Filter(): puri, subrng = self._parse_predicate_expression(node_type, pred) # track predicate uris suburi.add(puri) - try: - # check for more generic range - if rng is None or subrng > rng: - rng = subrng - # check range consistency - if not subrng <= rng and not subrng >= rng: - raise errors.ConsistencyError(f'ranges {subrng} and {rng} are not related') - except TypeError as err: # subrng and rng are not comparable - raise errors.ConsistencyError(f'ranges {subrng} and {rng} are not related') from err + # check for more generic range + if rng is None or subrng > rng: + rng = subrng + # check range consistency + if not subrng <= rng and not subrng >= rng: + raise errors.ConsistencyError(f'ranges {subrng} and {rng} are not related') # return joint predicate expression and next range # OneOf guarantees at least one expression, rng is always a bsc.Vertex. # mypy does not realize this, hence we ignore the warning. diff --git a/test/schema/test_types.py b/test/schema/test_types.py index 26da270..1eeafa1 100644 --- a/test/schema/test_types.py +++ b/test/schema/test_types.py @@ -140,30 +140,31 @@ class TestType(unittest.TestCase): self.assertFalse(bike >= bicycle) self.assertFalse(bike == bicycle) - # can compare types along the class hierarchy + # comparing different classes returns False ... + # ... when classes are hierarchically related class Foo(_Type): pass foo = Foo('Foo', bike) - self.assertTrue(foo < bike) - self.assertTrue(foo <= bike) + self.assertFalse(foo < bike) + self.assertFalse(foo <= bike) self.assertFalse(foo > bike) self.assertFalse(foo >= bike) # goes both ways self.assertFalse(bike < foo) self.assertFalse(bike <= foo) - self.assertTrue(bike > foo) - self.assertTrue(bike >= foo) - # cannot compare unrelated classes + self.assertFalse(bike > foo) + self.assertFalse(bike >= foo) + # ... when classes are unrelated class Bar(_Type): pass bar = Bar('Bar', bike) - self.assertRaises(TypeError, operator.lt, foo, bar) - self.assertRaises(TypeError, operator.le, foo, bar) - self.assertRaises(TypeError, operator.gt, foo, bar) - self.assertRaises(TypeError, operator.ge, foo, bar) + self.assertFalse(foo < bar) + self.assertFalse(foo <= bar) + self.assertFalse(foo > bar) + self.assertFalse(foo >= bar) # goes both ways - self.assertRaises(TypeError, operator.lt, bar, foo) - self.assertRaises(TypeError, operator.le, bar, foo) - self.assertRaises(TypeError, operator.gt, bar, foo) - self.assertRaises(TypeError, operator.ge, bar, foo) + self.assertFalse(bar < foo) + self.assertFalse(bar <= foo) + self.assertFalse(bar > foo) + self.assertFalse(bar >= foo) class TestPredicate(unittest.TestCase): @@ -262,7 +263,7 @@ class TestPredicate(unittest.TestCase): # range must be subtype of parent's range self.assertRaises(errors.ConsistencyError, tag.child, ns.bse.foo, range=n_root) self.assertRaises(errors.ConsistencyError, tag.child, ns.bse.foo, range=Node(ns.bsfs.Image, n_root)) - self.assertRaises(TypeError, tag.child, ns.bse.foo, range=Literal(ns.bsfs.Tag, l_root)) + self.assertRaises(errors.ConsistencyError, tag.child, ns.bse.foo, range=Literal(ns.bsfs.Tag, l_root)) # range can be subtyped from ROOT_VERTEX to Node or Literal self.assertEqual(n_root, root.child(ns.bse.foo, range=n_root).range) self.assertEqual(l_root, root.child(ns.bse.foo, range=l_root).range) @@ -370,7 +371,7 @@ class TestFeature(unittest.TestCase): # range must be subtype of parent's range self.assertRaises(errors.ConsistencyError, colors.child, ns.bse.foo, range=Literal(ns.bsfs.Literal, None)) self.assertRaises(errors.ConsistencyError, colors.child, ns.bse.foo, range=Literal(ns.bsfs.foo, Literal(ns.bsfs.Literal, None))) - self.assertRaises(TypeError, colors.child, ns.bse.foo, range=Node(ns.bsfs.Tag, n_root)) + self.assertRaises(errors.ConsistencyError, colors.child, ns.bse.foo, range=Node(ns.bsfs.Tag, n_root)) ## main ## -- cgit v1.2.3 From ccaee71e2b6135d3b324fe551c8652940b67aab3 Mon Sep 17 00:00:00 2001 From: Matthias Baumgartner Date: Sun, 15 Jan 2023 20:57:42 +0100 Subject: Feature as Literal instead of Predicate subtype --- bsfs/schema/__init__.py | 7 +- bsfs/schema/schema.py | 4 +- bsfs/schema/serialize.py | 83 +++---- bsfs/schema/types.py | 162 +++++++------- test/graph/test_nodes.py | 4 +- test/graph/test_resolve.py | 2 + test/query/test_validator.py | 2 + test/schema/test_schema.py | 34 +-- test/schema/test_serialize.py | 308 +++++++------------------- test/schema/test_types.py | 52 +---- test/triple_store/sparql/test_parse_filter.py | 5 +- test/triple_store/sparql/test_sparql.py | 8 +- 12 files changed, 257 insertions(+), 414 deletions(-) diff --git a/bsfs/schema/__init__.py b/bsfs/schema/__init__.py index 31d7d61..f53512e 100644 --- a/bsfs/schema/__init__.py +++ b/bsfs/schema/__init__.py @@ -10,8 +10,11 @@ import typing # inner-module imports from .schema import Schema from .serialize import from_string, to_string -from .types import Literal, Node, Predicate, Vertex, \ - ROOT_FEATURE, ROOT_LITERAL, ROOT_NODE, ROOT_NUMBER, ROOT_PREDICATE, ROOT_VERTEX +from .types import Literal, Node, Predicate, Vertex, Feature, \ + ROOT_VERTEX, ROOT_NODE, ROOT_LITERAL, \ + ROOT_NUMBER, ROOT_TIME, \ + ROOT_ARRAY, ROOT_FEATURE, \ + ROOT_PREDICATE # exports __all__: typing.Sequence[str] = ( diff --git a/bsfs/schema/schema.py b/bsfs/schema/schema.py index bc50d4e..8d9a821 100644 --- a/bsfs/schema/schema.py +++ b/bsfs/schema/schema.py @@ -70,7 +70,9 @@ class Schema(): predicates.add(types.ROOT_PREDICATE) # add minimally necessary types to the schema literals.add(types.ROOT_NUMBER) - predicates.add(types.ROOT_FEATURE) + literals.add(types.ROOT_TIME) + literals.add(types.ROOT_ARRAY) + literals.add(types.ROOT_FEATURE) # FIXME: ensure that types derive from the right root? diff --git a/bsfs/schema/serialize.py b/bsfs/schema/serialize.py index a566d65..8b31737 100644 --- a/bsfs/schema/serialize.py +++ b/bsfs/schema/serialize.py @@ -35,13 +35,27 @@ def from_string(schema_str: str) -> schema.Schema: graph.parse(data=schema_str, format='turtle') # helper functions + # FIXME: type annotation + def _fetch_value(subject: URI, predicate: rdflib.URIRef, value_factory) -> typing.Optional[typing.Any]: + """Fetch the object of a given subject and predicate. + Raises a `errors.ConsistencyError` if multiple objects match. + """ + values = list(graph.objects(rdflib.URIRef(subject), predicate)) + if len(values) == 0: + return None + if len(values) == 1: + return value_factory(values[0]) + raise errors.ConsistencyError( + f'{subject} has multiple values for predicate {str(predicate)}, expected zero or one') + def _convert(value): """Convert the subject type from rdflib to a bsfs native type.""" if isinstance(value, rdflib.Literal): return value.value if isinstance(value, rdflib.URIRef): return URI(value) - raise errors.UnreachableError(f'expected Literal or URIRef, found {typename(value)}') + # value is neither a node nor a literal, but e.g. a blank node + raise errors.BackendError(f'expected Literal or URIRef, found {typename(value)}') def _fetch_hierarchically(factory, curr): """Walk through a rdfs:subClassOf hierarchy, creating symbols along the way.""" @@ -71,30 +85,36 @@ def from_string(schema_str: str) -> schema.Schema: raise errors.ConsistencyError('inconsistent nodes') # fetch literals - literals = set(_fetch_hierarchically(types.Literal, types.ROOT_LITERAL)) + def _build_literal(uri, parent, **annotations): + """Literal factory.""" + # break out on root feature type + if uri == types.ROOT_FEATURE.uri: + return types.ROOT_FEATURE + # handle feature types + if isinstance(parent, types.Feature): + # clean annotations + annotations.pop(ns.bsfs.dimension, None) + annotations.pop(ns.bsfs.dtype, None) + annotations.pop(ns.bsfs.distance, None) + # get dimension + dimension = _fetch_value(uri, rdflib.URIRef(ns.bsfs.dimension), int) + # get dtype + dtype = _fetch_value(uri, rdflib.URIRef(ns.bsfs.dtype), URI) + # get distance + distance = _fetch_value(uri, rdflib.URIRef(ns.bsfs.distance), URI) + # return feature + return parent.child(URI(uri), dtype=dtype, dimension=dimension, distance=distance, **annotations) + # handle non-feature types + return parent.child(URI(uri), **annotations) + + literals = set(_fetch_hierarchically(_build_literal, types.ROOT_LITERAL)) literals_lut = {lit.uri: lit for lit in literals} if len(literals_lut) != len(literals): raise errors.ConsistencyError('inconsistent literals') # fetch predicates - # FIXME: type annotation - def _fetch_value(subject: URI, predicate: rdflib.URIRef, value_factory) -> typing.Optional[typing.Any]: - """Fetch the object of a given subject and predicate. - Raises a `errors.ConsistencyError` if multiple objects match. - """ - values = list(graph.objects(rdflib.URIRef(subject), predicate)) - if len(values) == 0: - return None - if len(values) == 1: - return value_factory(values[0]) - raise errors.ConsistencyError( - f'{subject} has multiple values for predicate {str(predicate)}, expected zero or one') - def _build_predicate(uri, parent, **annotations): """Predicate factory.""" - # break out on root feature type - if uri == types.ROOT_FEATURE.uri: - return types.ROOT_FEATURE # clean annotations annotations.pop(ns.rdfs.domain, None) annotations.pop(ns.rdfs.range, None) @@ -113,23 +133,9 @@ def from_string(schema_str: str) -> schema.Schema: rng = nodes_lut.get(rng, literals_lut.get(rng)) # get unique unique = _fetch_value(uri, rdflib.URIRef(ns.bsfs.unique), bool) - # handle feature types - if isinstance(parent, types.Feature): - # clean annotations - annotations.pop(ns.bsfs.dimension, None) - annotations.pop(ns.bsfs.dtype, None) - annotations.pop(ns.bsfs.distance, None) - # get dimension - dimension = _fetch_value(uri, rdflib.URIRef(ns.bsfs.dimension), int) - # get dtype - dtype = _fetch_value(uri, rdflib.URIRef(ns.bsfs.dtype), URI) - # get distance - distance = _fetch_value(uri, rdflib.URIRef(ns.bsfs.distance), URI) - # return feature - return parent.child(URI(uri), domain=dom, range=rng, unique=unique, - dtype=dtype, dimension=dimension, distance=distance, **annotations) - # handle non-feature predicate + # build predicate return parent.child(URI(uri), domain=dom, range=rng, unique=unique, **annotations) + predicates = _fetch_hierarchically(_build_predicate, types.ROOT_PREDICATE) return schema.Schema(predicates, nodes, literals) @@ -214,9 +220,12 @@ def to_string(schema_inst: schema.Schema, fmt: str = 'turtle') -> str: def _parse(node: types._Type) -> T_TRIPLE: """Emit all properties of a type.""" - if isinstance(node, types._Type): # pylint: disable=protected-access - # NOTE: all nodes are _Type - yield from _type(node) + # check arg + if not isinstance(node, types._Type): # pylint: disable=protected-access + raise TypeError(node) + # emit _Type essentials + yield from _type(node) + # emit properties of derived types if isinstance(node, types.Predicate): yield from _predicate(node) if isinstance(node, types.Feature): diff --git a/bsfs/schema/types.py b/bsfs/schema/types.py index 95dc66a..3a2e10c 100644 --- a/bsfs/schema/types.py +++ b/bsfs/schema/types.py @@ -226,10 +226,70 @@ class Node(Vertex): class Literal(Vertex): """Literal type.""" parent: typing.Optional['Literal'] - def __init__(self, uri: URI, parent: typing.Optional['Literal'] ,**kwargs): + def __init__(self, uri: URI, parent: typing.Optional['Literal'], **kwargs): super().__init__(uri, parent, **kwargs) +class Feature(Literal): + """Feature type.""" + + # Number of feature vector dimensions. + dimension: int + + # Feature vector datatype. + dtype: URI + + # Distance measure to compare feature vectors. + distance: URI + + def __init__( + self, + # Type members + uri: URI, + parent: typing.Optional[Literal], + # Feature members + dimension: int, + dtype: URI, + distance: URI, + **kwargs, + ): + super().__init__(uri, parent, **kwargs) + self.dimension = int(dimension) + self.dtype = URI(dtype) + self.distance = URI(distance) + + def __hash__(self) -> int: + return hash((super().__hash__(), self.dimension, self.dtype, self.distance)) + + def __eq__(self, other: typing.Any) -> bool: + return super().__eq__(other) \ + and self.dimension == other.dimension \ + and self.dtype == other.dtype \ + and self.distance == other.distance + + def child( + self, + uri: URI, + dimension: typing.Optional[int] = None, + dtype: typing.Optional[URI] = None, + distance: typing.Optional[URI] = None, + **kwargs, + ): + """Return a child of the current class.""" + if dimension is None: + dimension = self.dimension + if dtype is None: + dtype = self.dtype + if distance is None: + distance = self.distance + return super().child( + uri=uri, + dimension=dimension, + dtype=dtype, + distance=distance, + **kwargs, + ) + class Predicate(_Type): """Predicate base type.""" @@ -304,77 +364,6 @@ class Predicate(_Type): ) -class Feature(Predicate): - """Feature base type.""" - - # Number of feature vector dimensions. - dimension: int - - # Feature vector datatype. - dtype: URI - - # Distance measure to compare feature vectors. - distance: URI - - def __init__( - self, - # Type members - uri: URI, - parent: typing.Optional[Predicate], - # Predicate members - domain: Node, - range: Literal, # pylint: disable=redefined-builtin - unique: bool, - # Feature members - dimension: int, - dtype: URI, - distance: URI, - **kwargs, - ): - super().__init__(uri, parent, domain, range, unique, **kwargs) - self.dimension = int(dimension) - self.dtype = URI(dtype) - self.distance = URI(distance) - - def __hash__(self) -> int: - return hash((super().__hash__(), self.dimension, self.dtype, self.distance)) - - def __eq__(self, other: typing.Any) -> bool: - return super().__eq__(other) \ - and self.dimension == other.dimension \ - and self.dtype == other.dtype \ - and self.distance == other.distance - - def child( - self, - uri: URI, - domain: typing.Optional[Node] = None, - range: typing.Optional[Vertex] = None, # pylint: disable=redefined-builtin - unique: typing.Optional[bool] = None, - dimension: typing.Optional[int] = None, - dtype: typing.Optional[URI] = None, - distance: typing.Optional[URI] = None, - **kwargs, - ): - """Return a child of the current class.""" - if dimension is None: - dimension = self.dimension - if dtype is None: - dtype = self.dtype - if distance is None: - distance = self.distance - return super().child( - uri=uri, - domain=domain, - range=range, - unique=unique, - dimension=dimension, - dtype=dtype, - distance=distance, - **kwargs, - ) - - # essential vertices ROOT_VERTEX = Vertex( uri=ns.bsfs.Vertex, @@ -396,24 +385,31 @@ ROOT_NUMBER = Literal( parent=ROOT_LITERAL, ) -# essential predicates -ROOT_PREDICATE = Predicate( - uri=ns.bsfs.Predicate, - parent=None, - domain=ROOT_NODE, - range=ROOT_VERTEX, - unique=False, +ROOT_TIME = Literal( + uri=ns.bsfs.Time, + parent=ROOT_LITERAL, + ) + +ROOT_ARRAY = Literal( + uri=ns.bsfs.Array, + parent=ROOT_LITERAL, ) ROOT_FEATURE = Feature( uri=ns.bsfs.Feature, - parent=ROOT_PREDICATE, - domain=ROOT_NODE, - range=ROOT_LITERAL, - unique=False, + parent=ROOT_ARRAY, dimension=1, dtype=ns.bsfs.f16, distance=ns.bsfs.euclidean, ) +# essential predicates +ROOT_PREDICATE = Predicate( + uri=ns.bsfs.Predicate, + parent=None, + domain=ROOT_NODE, + range=ROOT_VERTEX, + unique=False, + ) + ## EOF ## diff --git a/test/graph/test_nodes.py b/test/graph/test_nodes.py index 81da60f..2870f35 100644 --- a/test/graph/test_nodes.py +++ b/test/graph/test_nodes.py @@ -75,12 +75,14 @@ class TestNodes(unittest.TestCase): ''') self.schema_triples = { # schema hierarchy - (rdflib.URIRef(ns.bsfs.Feature), rdflib.RDFS.subClassOf, rdflib.URIRef(ns.bsfs.Predicate)), (rdflib.URIRef(ns.bsfs.Entity), rdflib.RDFS.subClassOf, rdflib.URIRef(ns.bsfs.Node)), (rdflib.URIRef(ns.bsfs.Tag), rdflib.RDFS.subClassOf, rdflib.URIRef(ns.bsfs.Node)), (rdflib.URIRef(ns.bsfs.User), rdflib.RDFS.subClassOf, rdflib.URIRef(ns.bsfs.Node)), (rdflib.URIRef(ns.xsd.string), rdflib.RDFS.subClassOf, rdflib.URIRef(ns.bsfs.Literal)), + (rdflib.URIRef(ns.bsfs.Array), rdflib.RDFS.subClassOf, rdflib.URIRef(ns.bsfs.Literal)), + (rdflib.URIRef(ns.bsfs.Feature), rdflib.RDFS.subClassOf, rdflib.URIRef(ns.bsfs.Array)), (rdflib.URIRef(ns.bsfs.Number), rdflib.RDFS.subClassOf, rdflib.URIRef(ns.bsfs.Literal)), + (rdflib.URIRef(ns.bsfs.Time), rdflib.RDFS.subClassOf, rdflib.URIRef(ns.bsfs.Literal)), (rdflib.URIRef(ns.xsd.integer), rdflib.RDFS.subClassOf, rdflib.URIRef(ns.bsfs.Number)), (rdflib.URIRef(ns.bsm.t_created), rdflib.RDFS.subClassOf, rdflib.URIRef(ns.bsfs.Predicate)), (rdflib.URIRef(ns.bse.comment), rdflib.RDFS.subClassOf, rdflib.URIRef(ns.bsfs.Predicate)), diff --git a/test/graph/test_resolve.py b/test/graph/test_resolve.py index 9cde38e..0861a53 100644 --- a/test/graph/test_resolve.py +++ b/test/graph/test_resolve.py @@ -42,6 +42,8 @@ class TestFilter(unittest.TestCase): bsfs:Tag rdfs:subClassOf bsfs:Node . xsd:string rdfs:subClassOf bsfs:Literal . bsfs:Number rdfs:subClassOf bsfs:Literal . + bsfs:Array rdfs:subClassOf bsfs:Literal . + bsfs:Feature rdfs:subClassOf bsfs:Array . xsd:integer rdfs:subClassOf bsfs:Number . bse:comment rdfs:subClassOf bsfs:Predicate ; diff --git a/test/query/test_validator.py b/test/query/test_validator.py index ea56a57..63ead52 100644 --- a/test/query/test_validator.py +++ b/test/query/test_validator.py @@ -34,6 +34,8 @@ class TestFilter(unittest.TestCase): bsfs:Tag rdfs:subClassOf bsfs:Node . xsd:string rdfs:subClassOf bsfs:Literal . bsfs:Number rdfs:subClassOf bsfs:Literal . + bsfs:Array rdfs:subClassOf bsfs:Literal . + bsfs:Feature rdfs:subClassOf bsfs:Array . xsd:integer rdfs:subClassOf bsfs:Number . bse:comment rdfs:subClassOf bsfs:Predicate ; diff --git a/test/schema/test_schema.py b/test/schema/test_schema.py index c19c226..32dbc93 100644 --- a/test/schema/test_schema.py +++ b/test/schema/test_schema.py @@ -66,30 +66,32 @@ class TestSchema(unittest.TestCase): # literals self.l_root = types.ROOT_LITERAL self.l_number = types.ROOT_NUMBER + self.l_array = types.ROOT_ARRAY + self.l_time = types.ROOT_TIME self.l_string = self.l_root.child(ns.xsd.string) self.l_integer = self.l_root.child(ns.xsd.integer) self.l_unused = self.l_root.child(ns.xsd.boolean) - self.literals = [self.l_root, self.l_number, self.l_string, self.l_integer, self.l_unused] + self.f_root = types.ROOT_FEATURE + self.literals = [self.l_root, self.l_array, self.f_root, self.l_number, self.l_time, self.l_string, self.l_integer, self.l_unused] # predicates self.p_root = types.ROOT_PREDICATE - self.f_root = types.ROOT_FEATURE self.p_tag = self.p_root.child(ns.bse.tag, self.n_ent, self.n_tag, False) self.p_group = self.p_tag.child(ns.bse.group, self.n_img, self.n_tag, False) self.p_comment = self.p_root.child(ns.bse.comment, self.n_root, self.l_string, True) - self.predicates = [self.p_root, self.f_root, self.p_tag, self.p_group, self.p_comment] + self.predicates = [self.p_root, self.p_tag, self.p_group, self.p_comment] def test_construction(self): # no args yields a minimal schema schema = Schema() self.assertSetEqual(set(schema.nodes()), {self.n_root}) - self.assertSetEqual(set(schema.literals()), {self.l_root, self.l_number}) - self.assertSetEqual(set(schema.predicates()), {self.p_root, self.f_root}) + self.assertSetEqual(set(schema.literals()), {self.l_root, self.l_number, self.l_array, self.l_time, self.f_root}) + self.assertSetEqual(set(schema.predicates()), {self.p_root}) # nodes and literals are optional schema = Schema(self.predicates) self.assertSetEqual(set(schema.nodes()), {self.n_root, self.n_ent, self.n_img, self.n_tag}) - self.assertSetEqual(set(schema.literals()), {self.l_root, self.l_string, self.l_number}) + self.assertSetEqual(set(schema.literals()), {self.l_root, self.l_string, self.l_number, self.l_time, self.l_array, self.f_root}) self.assertSetEqual(set(schema.predicates()), set(self.predicates)) # predicates, nodes, and literals are respected @@ -110,21 +112,21 @@ class TestSchema(unittest.TestCase): # literals are complete schema = Schema(self.predicates, self.nodes, None) - self.assertSetEqual(set(schema.literals()), {self.l_root, self.l_string, self.l_number}) + self.assertSetEqual(set(schema.literals()), {self.l_root, self.l_string, self.l_number, self.l_array, self.l_time, self.f_root}) schema = Schema(self.predicates, self.nodes, []) - self.assertSetEqual(set(schema.literals()), {self.l_root, self.l_string, self.l_number}) + self.assertSetEqual(set(schema.literals()), {self.l_root, self.l_string, self.l_number, self.l_array, self.l_time, self.f_root}) schema = Schema(self.predicates, self.nodes, [self.l_string]) - self.assertSetEqual(set(schema.literals()), {self.l_root, self.l_string, self.l_number}) + self.assertSetEqual(set(schema.literals()), {self.l_root, self.l_string, self.l_number, self.l_array, self.l_time, self.f_root}) schema = Schema(self.predicates, self.nodes, [self.l_integer]) - self.assertSetEqual(set(schema.literals()), {self.l_root, self.l_string, self.l_integer, self.l_number}) + self.assertSetEqual(set(schema.literals()), {self.l_root, self.l_string, self.l_integer, self.l_number, self.l_array, self.l_time, self.f_root}) schema = Schema(self.predicates, self.nodes, [self.l_integer, self.l_unused]) self.assertSetEqual(set(schema.literals()), set(self.literals)) # predicates are complete schema = Schema([], self.nodes, self.literals) - self.assertSetEqual(set(schema.predicates()), {self.p_root, self.f_root}) + self.assertSetEqual(set(schema.predicates()), {self.p_root}) schema = Schema([self.p_group], self.nodes, self.literals) - self.assertSetEqual(set(schema.predicates()), {self.p_root, self.f_root, self.p_tag, self.p_group}) + self.assertSetEqual(set(schema.predicates()), {self.p_root, self.p_tag, self.p_group}) schema = Schema([self.p_group, self.p_comment], self.nodes, self.literals) self.assertSetEqual(set(schema.predicates()), set(self.predicates)) @@ -176,14 +178,14 @@ class TestSchema(unittest.TestCase): self.assertEqual(str(Schema(self.predicates, self.nodes, self.literals)), 'Schema()') # repr conversion with only default nodes, literals, and predicates n = [ns.bsfs.Node] - l = [ns.bsfs.Literal, ns.bsfs.Number] - p = [ns.bsfs.Feature, ns.bsfs.Predicate] + l = [ns.bsfs.Array, ns.bsfs.Feature, ns.bsfs.Literal, ns.bsfs.Number, ns.bsfs.Time] + p = [ns.bsfs.Predicate] self.assertEqual(repr(Schema()), f'Schema({n}, {l}, {p})') self.assertEqual(repr(Schema([], [], [])), f'Schema({n}, {l}, {p})') # repr conversion n = [ns.bsfs.Entity, ns.bsfs.Image, ns.bsfs.Node, ns.bsfs.Tag, ns.bsfs.Unused] - l = [ns.bsfs.Literal, ns.bsfs.Number, ns.xsd.boolean, ns.xsd.integer, ns.xsd.string] - p = [ns.bse.comment, ns.bse.group, ns.bse.tag, ns.bsfs.Feature, ns.bsfs.Predicate] + l = [ns.bsfs.Array, ns.bsfs.Feature, ns.bsfs.Literal, ns.bsfs.Number, ns.bsfs.Time, ns.xsd.boolean, ns.xsd.integer, ns.xsd.string] + p = [ns.bse.comment, ns.bse.group, ns.bse.tag, ns.bsfs.Predicate] self.assertEqual(repr(Schema(self.predicates, self.nodes, self.literals)), f'Schema({n}, {l}, {p})') def test_equality(self): diff --git a/test/schema/test_serialize.py b/test/schema/test_serialize.py index 205150a..fc6b20a 100644 --- a/test/schema/test_serialize.py +++ b/test/schema/test_serialize.py @@ -581,151 +581,60 @@ class TestFromString(unittest.TestCase): def test_feature(self): - # domain must be defined - self.assertRaises(errors.ConsistencyError, from_string, ''' - prefix rdfs: - prefix xsd: - prefix bsfs: - prefix bse: - - bsfs:Feature rdfs:subClassOf bsfs:Predicate . - bsfs:array rdfs:subClassOf bsfs:Literal . - - bse:colors rdfs:subClassOf bsfs:Feature ; - rdfs:domain bsfs:Entity ; # undefined symbol - rdfs:range bsfs:array ; - bsfs:unique "false"^^xsd:boolean . - ''') - # domain cannot be a literal - self.assertRaises(errors.ConsistencyError, from_string, ''' - prefix rdfs: - prefix xsd: - prefix bsfs: - prefix bse: - - bsfs:Feature rdfs:subClassOf bsfs:Predicate . - bsfs:Entity rdfs:subClassOf bsfs:Literal . - bsfs:array rdfs:subClassOf bsfs:Literal . - - bse:colors rdfs:subClassOf bsfs:Feature ; - rdfs:domain bsfs:Entity ; # literal instead of node - rdfs:range bsfs:array ; - bsfs:unique "false"^^xsd:boolean . - ''') - - # range must be defined - self.assertRaises(errors.ConsistencyError, from_string, ''' - prefix rdfs: - prefix xsd: - prefix bsfs: - prefix bse: - - bsfs:Feature rdfs:subClassOf bsfs:Predicate . - bsfs:Entity rdfs:subClassOf bsfs:Node . - - bse:colors rdfs:subClassOf bsfs:Feature ; - rdfs:domain bsfs:Entity ; - rdfs:range bsfs:array ; # undefined symbol - bsfs:unique "false"^^xsd:boolean . - ''') - # range must be defined - self.assertRaises(errors.ConsistencyError, from_string, ''' + # additional features can be defined + f_colors = types.ROOT_FEATURE.child(ns.bsfs.Colors) + self.assertEqual(Schema(literals={f_colors}), from_string(''' prefix rdfs: prefix xsd: prefix bsfs: prefix bse: - bsfs:Feature rdfs:subClassOf bsfs:Predicate . - bsfs:Entity rdfs:subClassOf bsfs:Node . + bsfs:Array rdfs:subClassOf bsfs:Literal . + bsfs:Feature rdfs:subClassOf bsfs:Array. - bse:colors rdfs:subClassOf bsfs:Feature ; - rdfs:domain bsfs:Entity ; - rdfs:range bsfs:Foo ; # undefined symbol - bsfs:unique "false"^^xsd:boolean . - ''') - # range must be a node or a literal - self.assertRaises(errors.ConsistencyError, from_string, ''' - prefix rdfs: - prefix xsd: - prefix bsfs: - prefix bse: - - bsfs:Feature rdfs:subClassOf bsfs:Predicate . - bsfs:Entity rdfs:subClassOf bsfs:Node . - - bse:colors rdfs:subClassOf bsfs:Feature ; - rdfs:domain bsfs:Entity ; - rdfs:range bsfs:Predicate ; # invalid symbol - bsfs:unique "false"^^xsd:boolean . - ''') + bsfs:Colors rdfs:subClassOf bsfs:Feature . - # additional predicates can be defined - n_ent = types.ROOT_NODE.child(ns.bsfs.Entity) - l_array = types.ROOT_LITERAL.child(ns.bsfs.array) - p_comment = types.ROOT_FEATURE.child(ns.bse.colors, domain=n_ent, range=l_array, unique=False) - self.assertEqual(Schema({p_comment}), from_string(''' - prefix rdfs: - prefix xsd: - prefix bsfs: - prefix bse: - - bsfs:Feature rdfs:subClassOf bsfs:Predicate . - bsfs:Entity rdfs:subClassOf bsfs:Node . - bsfs:array rdfs:subClassOf bsfs:Literal . - - bse:colors rdfs:subClassOf bsfs:Feature ; - rdfs:domain bsfs:Entity ; - rdfs:range bsfs:array ; - bsfs:unique "false"^^xsd:boolean . ''')) # features inherit properties from parents - n_ent = types.ROOT_NODE.child(ns.bsfs.Entity) - l_array = types.ROOT_LITERAL.child(ns.bsfs.array) - l_string = types.ROOT_LITERAL.child(ns.xsd.string) - p_annotation = types.ROOT_FEATURE.child(ns.bsfs.Annotation, domain=n_ent, range=l_array, - dimension=1234, dtype=ns.xsd.string) - p_comment = p_annotation.child(ns.bse.colors, unique=True) - self.assertEqual(Schema({p_comment}), from_string(''' + f_colors = types.ROOT_FEATURE.child(ns.bsfs.Colors, dimension=1234, dtype=ns.bsfs.i32) + f_main_colors = f_colors.child(ns.bsfs.MainColor, distance=ns.bsfs.cosine, dtype=ns.bsfs.f16) + self.assertEqual(Schema(literals={f_colors, f_main_colors}), from_string(''' prefix rdfs: prefix xsd: prefix bsfs: prefix bse: - bsfs:Feature rdfs:subClassOf bsfs:Predicate . - bsfs:Entity rdfs:subClassOf bsfs:Node . - bsfs:array rdfs:subClassOf bsfs:Literal . + bsfs:Array rdfs:subClassOf bsfs:Literal . + bsfs:Feature rdfs:subClassOf bsfs:Array. - bsfs:Annotation rdfs:subClassOf bsfs:Feature ; # inherits defaults from bsfs:Feature - rdfs:domain bsfs:Entity ; - rdfs:range bsfs:array ; - bsfs:dimension "1234"^^xsd:integer ; - bsfs:dtype xsd:string . + bsfs:Colors rdfs:subClassOf bsfs:Feature ; # inherits distance from bsfs:Feature + bsfs:dimension "1234"^^xsd:integer ; # overwrites bsfs:Feature + bsfs:dtype bsfs:i32 . # overwrites bsfs:Feature + + bsfs:MainColor rdfs:subClassOf bsfs:Colors ; # inherits dimension from bsfs:Colors + bsfs:distance bsfs:cosine ; # overwrites bsfs:Feature + bsfs:dtype bsfs:f16 . # overwrites bsfs:Colors - bse:colors rdfs:subClassOf bsfs:Annotation ; # inherits domain/range/etc. from bsfs:Annotation - bsfs:unique "true"^^xsd:boolean . # overwrites bsfs:Predicate ''')) # feature definition can be split across multiple statements. # statements can be repeated - n_ent = types.ROOT_NODE.child(ns.bsfs.Entity) - p_foo = types.ROOT_FEATURE.child(ns.bse.foo, domain=n_ent, unique=True, - dimension=1234, dtype=ns.bsfs.f32) - self.assertEqual(Schema({p_foo}), from_string(''' + f_colors = types.ROOT_FEATURE.child(ns.bsfs.Colors, dimension=1234, dtype=ns.bsfs.f32) + self.assertEqual(Schema(literals={f_colors}), from_string(''' prefix rdfs: prefix xsd: prefix bsfs: prefix bse: - bsfs:Feature rdfs:subClassOf bsfs:Predicate . - bsfs:Entity rdfs:subClassOf bsfs:Node . + bsfs:Array rdfs:subClassOf bsfs:Literal . + bsfs:Feature rdfs:subClassOf bsfs:Array. - bse:foo rdfs:subClassOf bsfs:Feature ; - bsfs:unique "true"^^xsd:boolean ; + bsfs:Colors rdfs:subClassOf bsfs:Feature ; bsfs:dimension "1234"^^xsd:integer . - bse:foo rdfs:subClassOf bsfs:Feature ; - rdfs:domain bsfs:Entity ; + bsfs:Colors rdfs:subClassOf bsfs:Feature ; + bsfs:dimension "1234"^^xsd:integer ; # non-conflicting repetition bsfs:dtype bsfs:f32 . ''')) @@ -736,75 +645,14 @@ class TestFromString(unittest.TestCase): prefix bsfs: prefix bse: - bsfs:Feature rdfs:subClassOf bsfs:Predicate . - bsfs:Annotation rdfs:subClassOf bsfs:Feature . - bsfs:Entity rdfs:subClassOf bsfs:Node . + bsfs:Array rdfs:subClassOf bsfs:Literal . + bsfs:Feature rdfs:subClassOf bsfs:Array. + bsfs:ColorSpace rdfs:subClassOf bsfs:Feature . - bse:foo rdfs:subClassOf bsfs:Feature ; - rdfs:domain bsfs:Node ; - bsfs:unique "false"^^xsd:boolean . - - bse:foo rdfs:subClassOf bsfs:Annotation ; - rdfs:domain bsfs:Node ; - bsfs:unique "false"^^xsd:boolean . + bsfs:Colors rdfs:subClassOf bsfs:Feature . + bsfs:Colors rdfs:subClassOf bsfs:ColorSpace . ''') - # cannot assign multiple conflicting domains to the same feature - self.assertRaises(errors.ConsistencyError, from_string, ''' - prefix rdfs: - prefix xsd: - prefix bsfs: - prefix bse: - - bsfs:Feature rdfs:subClassOf bsfs:Predicate . - bsfs:Entity rdfs:subClassOf bsfs:Node . - - bse:foo rdfs:subClassOf bsfs:Feature ; - rdfs:domain bsfs:Node ; - bsfs:unique "false"^^xsd:boolean . - - bse:foo rdfs:subClassOf bsfs:Feature ; - rdfs:domain bsfs:Entity . # conflicting domain - ''') - # cannot assign multiple conflicting ranges to the same feature - self.assertRaises(errors.ConsistencyError, from_string, ''' - prefix rdfs: - prefix xsd: - prefix bsfs: - prefix bse: - - bsfs:Feature rdfs:subClassOf bsfs:Predicate . - bsfs:Entity rdfs:subClassOf bsfs:Node . - bsfs:array rdfs:subClassOf bsfs:Literal . - bsfs:large_array rdfs:subClassOf bsfs:array . - bsfs:small_array rdfs:subClassOf bsfs:array . - - bse:foo rdfs:subClassOf bsfs:Feature ; - rdfs:domain bsfs:Node ; - rdfs:range bsfs:large_array ; - bsfs:unique "false"^^xsd:boolean . - - bse:foo rdfs:subClassOf bsfs:Feature ; - rdfs:range bsfs:small_array . # conflicting range - ''') - # cannot assign multiple conflicting uniques to the same feature - self.assertRaises(errors.ConsistencyError, from_string, ''' - prefix rdfs: - prefix xsd: - prefix bsfs: - prefix bse: - - bsfs:Feature rdfs:subClassOf bsfs:Predicate . - bsfs:Entity rdfs:subClassOf bsfs:Node . - - bse:foo rdfs:subClassOf bsfs:Feature ; - rdfs:domain bsfs:Node ; - rdfs:range bsfs:Node ; - bsfs:unique "false"^^xsd:boolean . - - bse:foo rdfs:subClassOf bsfs:Feature ; - bsfs:unique "true"^^xsd:boolean . # conflicting unique - ''') # cannot assign multiple conflicting dimensions to the same feature self.assertRaises(errors.ConsistencyError, from_string, ''' prefix rdfs: @@ -812,15 +660,15 @@ class TestFromString(unittest.TestCase): prefix bsfs: prefix bse: - bsfs:Feature rdfs:subClassOf bsfs:Predicate . - bsfs:Entity rdfs:subClassOf bsfs:Node . + bsfs:Array rdfs:subClassOf bsfs:Literal . + bsfs:Feature rdfs:subClassOf bsfs:Array. - bse:foo rdfs:subClassOf bsfs:Feature ; - rdfs:domain bsfs:Node ; + bsfs:Colors rdfs:subClassOf bsfs:Feature ; bsfs:dimension "1234"^^xsd:integer . - bse:foo rdfs:subClassOf bsfs:Feature ; + bsfs:Colors rdfs:subClassOf bsfs:Feature ; bsfs:dimension "4321"^^xsd:integer . # conflicting dimension + ''') # cannot assign multiple conflicting dtypes to the same feature self.assertRaises(errors.ConsistencyError, from_string, ''' @@ -829,14 +677,13 @@ class TestFromString(unittest.TestCase): prefix bsfs: prefix bse: - bsfs:Feature rdfs:subClassOf bsfs:Predicate . - bsfs:Entity rdfs:subClassOf bsfs:Node . + bsfs:Array rdfs:subClassOf bsfs:Literal . + bsfs:Feature rdfs:subClassOf bsfs:Array. - bse:foo rdfs:subClassOf bsfs:Feature ; - rdfs:domain bsfs:Node ; + bsfs:Colors rdfs:subClassOf bsfs:Feature ; bsfs:dtype bsfs:f32 . - bse:foo rdfs:subClassOf bsfs:Feature ; + bsfs:Colors rdfs:subClassOf bsfs:Feature ; bsfs:dtype bsfs:f16 . # conflicting dtype ''') # cannot assign multiple conflicting distance metrics to the same feature @@ -846,14 +693,13 @@ class TestFromString(unittest.TestCase): prefix bsfs: prefix bse: - bsfs:Feature rdfs:subClassOf bsfs:Predicate . - bsfs:Entity rdfs:subClassOf bsfs:Node . + bsfs:Array rdfs:subClassOf bsfs:Literal . + bsfs:Feature rdfs:subClassOf bsfs:Array. - bse:foo rdfs:subClassOf bsfs:Feature ; - rdfs:domain bsfs:Node ; + bsfs:Colors rdfs:subClassOf bsfs:Feature ; bsfs:distance bsfs:euclidean . - bse:foo rdfs:subClassOf bsfs:Feature ; + bsfs:Colors rdfs:subClassOf bsfs:Feature ; bsfs:distance bsfs:cosine . # conflicting distance ''') @@ -864,24 +710,28 @@ class TestFromString(unittest.TestCase): prefix bsfs: prefix bse: - bsfs:Feature rdfs:subClassOf bsfs:Predicate . - bse:colors rdfs:subClassOf bsfs:Feature ; + bsfs:Array rdfs:subClassOf bsfs:Literal . + bsfs:Feature rdfs:subClassOf bsfs:Array. + + bsfs:Colors rdfs:subClassOf bsfs:Feature ; bsfs:dimension "1234"^^xsd:integer . - ''').predicate(ns.bse.colors).annotations, {}) + ''').literal(ns.bsfs.Colors).annotations, {}) self.assertDictEqual(from_string(''' prefix rdfs: prefix xsd: prefix bsfs: prefix bse: - bsfs:Feature rdfs:subClassOf bsfs:Predicate . - bse:colors rdfs:subClassOf bsfs:Feature ; + bsfs:Array rdfs:subClassOf bsfs:Literal . + bsfs:Feature rdfs:subClassOf bsfs:Array. + + bsfs:Colors rdfs:subClassOf bsfs:Feature ; bsfs:dimension "1234"^^xsd:integer ; rdfs:label "hello world"^^xsd:string ; bsfs:foo "1234"^^xsd:integer . - ''').predicate(ns.bse.colors).annotations, { + ''').literal(ns.bsfs.Colors).annotations, { ns.rdfs.label: 'hello world', ns.bsfs.foo: 1234, }) @@ -904,14 +754,14 @@ class TestFromString(unittest.TestCase): p_comment = p_annotation.child(ns.bse.comment, range=l_string) # features f_colors = types.ROOT_FEATURE.child(URI('http://bsfs.ai/schema/Feature/colors_spatial'), - domain=n_ent, range=l_array, unique=True, dtype=ns.bsfs.f16, distance=ns.bsfs.euclidean) + dtype=ns.bsfs.f16, distance=ns.bsfs.euclidean) f_colors1234 = f_colors.child(URI('http://bsfs.ai/schema/Feature/colors_spatial#1234'), dimension=1024) f_colors4321 = f_colors.child(URI('http://bsfs.ai/schema/Feature/colors_spatial#4321'), dimension=2048) # schema ref = Schema( - {p_annotation, p_tag, p_group, p_comment, f_colors, f_colors1234, f_colors4321}, + {p_annotation, p_tag, p_group, p_comment}, {n_ent, n_tag, n_image}, - {l_string, l_integer, l_boolean}) + {l_string, l_integer, l_boolean, f_colors, f_colors1234, f_colors4321}) # load from string gen = from_string(''' # generic prefixes @@ -932,21 +782,19 @@ class TestFromString(unittest.TestCase): # literals xsd:string rdfs:subClassOf bsfs:Literal ; rdfs:label "A sequence of characters"^^xsd:string . - bsfs:array rdfs:subClassOf bsfs:Literal . + bsfs:Array rdfs:subClassOf bsfs:Literal . + bsfs:Feature rdfs:subClassOf bsfs:Array. bsfs:Number rdfs:subClassOf bsfs:Literal . xsd:integer rdfs:subClassOf bsfs:Number . xsd:boolean rdfs:subClassOf bsfs:Literal . + # abstract predicates bsfs:Annotation rdfs:subClassOf bsfs:Predicate ; rdfs:label "node annotation"^^xsd:string . - bsfs:Feature rdfs:subClassOf bsfs:Predicate . # feature instances rdfs:subClassOf bsfs:Feature ; - rdfs:domain bsfs:Entity ; - rdfs:range bsfs:array ; - bsfs:unique "true"^^xsd:boolean ; bsfs:dtype bsfs:f16 ; bsfs:distance bsfs:euclidean ; # annotations @@ -986,15 +834,22 @@ class TestFromString(unittest.TestCase): self.assertDictEqual(gen.node(ns.bsfs.Tag).annotations, {ns.rdfs.label: 'Tag'}) self.assertDictEqual(gen.literal(ns.xsd.string).annotations, {ns.rdfs.label: 'A sequence of characters'}) self.assertDictEqual(gen.predicate(ns.bsfs.Annotation).annotations, {ns.rdfs.label: 'node annotation'}) - self.assertDictEqual(gen.predicate(URI('http://bsfs.ai/schema/Feature/colors_spatial')).annotations, { + self.assertDictEqual(gen.literal(URI('http://bsfs.ai/schema/Feature/colors_spatial')).annotations, { ns.rdfs.label: 'ColorsSpatial instances. Dimension depends on instance.', ns.bsfs.first_arg: 1234, ns.bsfs.second_arg: 'hello world', }) - self.assertDictEqual(gen.predicate(URI('http://bsfs.ai/schema/Feature/colors_spatial#1234')).annotations, { + self.assertDictEqual(gen.literal(URI('http://bsfs.ai/schema/Feature/colors_spatial#1234')).annotations, { ns.rdfs.label: 'Main colors spatial instance'}) self.assertDictEqual(gen.predicate(ns.bse.tag).annotations, {ns.rdfs.label: 'connect entity to a tag'}) + # blank nodes result in an error + self.assertRaises(errors.BackendError, from_string, ''' + prefix rdfs: + prefix bsfs: + bsfs:Entity rdfs:subClassOf bsfs:Node ; + bsfs:foo _:bar . + ''') class TestToString(unittest.TestCase): @@ -1002,6 +857,11 @@ class TestToString(unittest.TestCase): def test_empty(self): self.assertEqual(Schema(), from_string(to_string(Schema()))) + def test_parse(self): + schema = Schema() + schema._nodes[ns.bsfs.Invalid] = 123 # NOTE: Access protected to force an invalid schema + self.assertRaises(TypeError, to_string, schema) + def test_literal(self): # root literals l_str = types.ROOT_LITERAL.child(ns.xsd.string) @@ -1120,37 +980,29 @@ class TestToString(unittest.TestCase): def test_feature(self): - # auxiliary types - n_ent = types.ROOT_NODE.child(ns.bsfs.Entity) - l_array = types.ROOT_LITERAL.child(ns.bsfs.array) # root features f_colors = types.ROOT_FEATURE.child(URI('http://bsfs.ai/schema/Feature/colors'), - range=l_array, unique=True, distance=ns.bsfs.cosine) + distance=ns.bsfs.cosine) # derived features f_colors1234 = f_colors.child(URI('http://bsfs.ai/schema/Feature/colors#1234'), - dimension=1024, domain=n_ent) # inherits range/dtype/distance + dimension=1024) # inherits dtype, distance f_colors4321 = f_colors.child(URI('http://bsfs.ai/schema/Feature/colors#4321'), - dimension=2048, distance=ns.bsfs.euclidean) # inherits domain/range/dtype + dimension=2048, distance=ns.bsfs.euclidean) # inherits dtype # create schema - schema = Schema({f_colors, f_colors1234, f_colors4321}) + schema = Schema(literals={f_colors, f_colors1234, f_colors4321}) schema_str = to_string(schema) # all symbols are serialized - self.assertIn('bsfs:Entity', schema_str) - self.assertIn('bsfs:array', schema_str) + self.assertIn('bsfs:Array', schema_str) self.assertIn('[^\.]*rdfs:domain[^\.]', schema_str)) self.assertIsNotNone(re.search(r'[^\.]*bsfs:dimension[^\.]', schema_str)) - self.assertIsNone(re.search(r'[^\.]*rdfs:range[^\.]', schema_str)) self.assertIsNone(re.search(r'[^\.]*bsfs:dtype[^\.]', schema_str)) self.assertIsNone(re.search(r'[^\.]*bsfs:distance[^\.]', schema_str)) self.assertIsNotNone(re.search(r'[^\.]*bsfs:dimension[^\.]', schema_str)) self.assertIsNotNone(re.search(r'[^\.]*bsfs:distance[^\.]', schema_str)) - self.assertIsNone(re.search(r'[^\.]*rdfs:domain[^\.]', schema_str)) - self.assertIsNone(re.search(r'[^\.]*rdfs:range[^\.]', schema_str)) self.assertIsNone(re.search(r'[^\.]*bsfs:dtype[^\.]', schema_str)) # unserialize yields the original schema self.assertEqual(schema, from_string(schema_str)) @@ -1163,11 +1015,11 @@ class TestToString(unittest.TestCase): ns.bsfs.bar: False, } f_colors = types.ROOT_FEATURE.child(URI('http://bsfs.ai/schema/Feature/colors'), - domain=n_ent, range=l_array, unique=True, dtype=ns.bsfs.f16, distance=ns.bsfs.euclidean, + dtype=ns.bsfs.f16, distance=ns.bsfs.euclidean, **annotations) self.assertDictEqual( annotations, - from_string(to_string(Schema({f_colors}))).predicate(URI('http://bsfs.ai/schema/Feature/colors')).annotations) + from_string(to_string(Schema(literals={f_colors}))).literal(URI('http://bsfs.ai/schema/Feature/colors')).annotations) ## main ## diff --git a/test/schema/test_types.py b/test/schema/test_types.py index 1eeafa1..c5895d2 100644 --- a/test/schema/test_types.py +++ b/test/schema/test_types.py @@ -274,21 +274,16 @@ class TestFeature(unittest.TestCase): n_root = Node(ns.bsfs.Node, None) l_root = Literal(ns.bsfs.Literal, None) # dimension, dtype, and distance are respected - feat = Feature(ns.bsfs.Feature, None, n_root, l_root, False, - 1234, ns.bsfs.float, ns.bsfs.euclidean) + feat = Feature(ns.bsfs.Feature, None, 1234, ns.bsfs.float, ns.bsfs.euclidean) self.assertEqual(1234, feat.dimension) self.assertEqual(ns.bsfs.float, feat.dtype) self.assertEqual(ns.bsfs.euclidean, feat.distance) def test_equality(self): n_ent = Node(ns.bsfs.Entity, Node(ns.bsfs.Node, None)) - l_array = Literal(ns.bsfs.array, Literal(ns.bsfs.Literal, None)) colors = Feature( uri=ns.bse.colors, parent=ROOT_FEATURE, - domain=n_ent, - range=l_array, - unique=False, dimension=1234, dtype=ns.bsfs.float, distance=ns.bsfs.euclidean, @@ -297,29 +292,25 @@ class TestFeature(unittest.TestCase): self.assertEqual(colors, colors) self.assertEqual(hash(colors), hash(colors)) # instance is equal to a clone - self.assertEqual(colors, Feature(ns.bse.colors, ROOT_FEATURE, n_ent, l_array, False, 1234, ns.bsfs.float, ns.bsfs.euclidean)) - self.assertEqual(hash(colors), hash(Feature(ns.bse.colors, ROOT_FEATURE, n_ent, l_array, False, 1234, ns.bsfs.float, ns.bsfs.euclidean))) + self.assertEqual(colors, Feature(ns.bse.colors, ROOT_FEATURE, 1234, ns.bsfs.float, ns.bsfs.euclidean)) + self.assertEqual(hash(colors), hash(Feature(ns.bse.colors, ROOT_FEATURE, 1234, ns.bsfs.float, ns.bsfs.euclidean))) # equality respects dimension - self.assertNotEqual(colors, Feature(ns.bse.colors, ROOT_FEATURE, n_ent, l_array, False, 4321, ns.bsfs.float, ns.bsfs.euclidean)) - self.assertNotEqual(hash(colors), hash(Feature(ns.bse.colors, ROOT_FEATURE, n_ent, l_array, False, 4321, ns.bsfs.float, ns.bsfs.euclidean))) + self.assertNotEqual(colors, Feature(ns.bse.colors, ROOT_FEATURE, 4321, ns.bsfs.float, ns.bsfs.euclidean)) + self.assertNotEqual(hash(colors), hash(Feature(ns.bse.colors, ROOT_FEATURE, 4321, ns.bsfs.float, ns.bsfs.euclidean))) # equality respects dtype - self.assertNotEqual(colors, Feature(ns.bse.colors, ROOT_FEATURE, n_ent, l_array, False, 1234, ns.bsfs.integer, ns.bsfs.euclidean)) - self.assertNotEqual(hash(colors), hash(Feature(ns.bse.colors, ROOT_FEATURE, n_ent, l_array, False, 1234, ns.bsfs.integer, ns.bsfs.euclidean))) + self.assertNotEqual(colors, Feature(ns.bse.colors, ROOT_FEATURE, 1234, ns.bsfs.integer, ns.bsfs.euclidean)) + self.assertNotEqual(hash(colors), hash(Feature(ns.bse.colors, ROOT_FEATURE, 1234, ns.bsfs.integer, ns.bsfs.euclidean))) # equality respects distance - self.assertNotEqual(colors, Feature(ns.bse.colors, ROOT_FEATURE, n_ent, l_array, False, 1234, ns.bsfs.float, ns.bsfs.cosine)) - self.assertNotEqual(hash(colors), hash(Feature(ns.bse.colors, ROOT_FEATURE, n_ent, l_array, False, 1234, ns.bsfs.float, ns.bsfs.cosine))) + self.assertNotEqual(colors, Feature(ns.bse.colors, ROOT_FEATURE, 1234, ns.bsfs.float, ns.bsfs.cosine)) + self.assertNotEqual(hash(colors), hash(Feature(ns.bse.colors, ROOT_FEATURE, 1234, ns.bsfs.float, ns.bsfs.cosine))) def test_child(self): n_root = Node(ns.bsfs.Node, None) n_ent = Node(ns.bsfs.Entity, n_root) l_root = Literal(ns.bsfs.Literal, None) - l_array = Literal(ns.bsfs.array, l_root) colors = Feature( uri=ns.bse.colors, parent=ROOT_FEATURE, - domain=n_ent, - range=l_array, - unique=False, dimension=1234, dtype=ns.bsfs.float, distance=ns.bsfs.euclidean, @@ -329,16 +320,6 @@ class TestFeature(unittest.TestCase): self.assertIsInstance(colors.child(ns.bse.foo), Feature) # uri is respected self.assertEqual(ns.bse.foo, colors.child(ns.bse.foo).uri) - # domain is respected - dom = Node(ns.bsfs.Image, n_ent) - self.assertEqual(dom, colors.child(ns.bse.foo, domain=dom).domain) - # range is respected - rng = Literal(ns.bse.foo, l_array) - self.assertEqual(rng, colors.child(ns.bse.foo, range=rng).range) - # cannot set range to None - self.assertEqual(l_array, colors.child(ns.bse.foo, range=None).range) - # unique is respected - self.assertTrue(colors.child(ns.bse.foo, unique=True).unique) # dimension is respected self.assertEqual(4321, colors.child(ns.bse.foo, dimension=4321).dimension) # dtype is respected @@ -351,13 +332,6 @@ class TestFeature(unittest.TestCase): 'bar': 123, }) - # domain is inherited from parent - self.assertEqual(n_root, ROOT_FEATURE.child(ns.bse.foo).domain) - self.assertEqual(n_ent, colors.child(ns.bse.foo).domain) - # range is inherited from parent - self.assertEqual(l_array, colors.child(ns.bse.foo).range) - # uniqueness is inherited from parent - self.assertFalse(colors.child(ns.bse.foo).unique) # dimension is inherited from parent self.assertEqual(1234, colors.child(ns.bse.foo).dimension) # dtype is inherited from parent @@ -365,14 +339,6 @@ class TestFeature(unittest.TestCase): # distance is inherited from parent self.assertEqual(ns.bsfs.euclidean, colors.child(ns.bse.foo).distance) - # domain must be subtype of parent's domain - self.assertRaises(errors.ConsistencyError, colors.child, ns.bse.foo, domain=n_root) - self.assertRaises(errors.ConsistencyError, colors.child, ns.bse.foo, domain=Node(ns.bsfs.Image, n_root)) - # range must be subtype of parent's range - self.assertRaises(errors.ConsistencyError, colors.child, ns.bse.foo, range=Literal(ns.bsfs.Literal, None)) - self.assertRaises(errors.ConsistencyError, colors.child, ns.bse.foo, range=Literal(ns.bsfs.foo, Literal(ns.bsfs.Literal, None))) - self.assertRaises(errors.ConsistencyError, colors.child, ns.bse.foo, range=Node(ns.bsfs.Tag, n_root)) - ## main ## diff --git a/test/triple_store/sparql/test_parse_filter.py b/test/triple_store/sparql/test_parse_filter.py index f6842c5..5c16f11 100644 --- a/test/triple_store/sparql/test_parse_filter.py +++ b/test/triple_store/sparql/test_parse_filter.py @@ -30,12 +30,15 @@ class TestParseFilter(unittest.TestCase): prefix bsfs: prefix bse: + bsfs:Array rdfs:subClassOf bsfs:Literal . + bsfs:Feature rdfs:subClassOf bsfs:Array . + bsfs:Number rdfs:subClassOf bsfs:Literal . + bsfs:Entity rdfs:subClassOf bsfs:Node . bsfs:Image rdfs:subClassOf bsfs:Entity . bsfs:Tag rdfs:subClassOf bsfs:Node . xsd:string rdfs:subClassOf bsfs:Literal . - bsfs:Number rdfs:subClassOf bsfs:Literal . xsd:integer rdfs:subClassOf bsfs:Number . bsfs:URI rdfs:subClassOf bsfs:Literal . diff --git a/test/triple_store/sparql/test_sparql.py b/test/triple_store/sparql/test_sparql.py index aa5dfc7..1f56a7e 100644 --- a/test/triple_store/sparql/test_sparql.py +++ b/test/triple_store/sparql/test_sparql.py @@ -63,12 +63,14 @@ class TestSparqlStore(unittest.TestCase): ''') self.schema_triples = { # schema hierarchy - (rdflib.URIRef(ns.bsfs.Feature), rdflib.RDFS.subClassOf, rdflib.URIRef(ns.bsfs.Predicate)), (rdflib.URIRef(ns.bsfs.Entity), rdflib.RDFS.subClassOf, rdflib.URIRef(ns.bsfs.Node)), (rdflib.URIRef(ns.bsfs.Tag), rdflib.RDFS.subClassOf, rdflib.URIRef(ns.bsfs.Node)), (rdflib.URIRef(ns.bsfs.User), rdflib.RDFS.subClassOf, rdflib.URIRef(ns.bsfs.Node)), (rdflib.URIRef(ns.xsd.string), rdflib.RDFS.subClassOf, rdflib.URIRef(ns.bsfs.Literal)), + (rdflib.URIRef(ns.bsfs.Array), rdflib.RDFS.subClassOf, rdflib.URIRef(ns.bsfs.Literal)), + (rdflib.URIRef(ns.bsfs.Feature), rdflib.RDFS.subClassOf, rdflib.URIRef(ns.bsfs.Array)), (rdflib.URIRef(ns.bsfs.Number), rdflib.RDFS.subClassOf, rdflib.URIRef(ns.bsfs.Literal)), + (rdflib.URIRef(ns.bsfs.Time), rdflib.RDFS.subClassOf, rdflib.URIRef(ns.bsfs.Literal)), (rdflib.URIRef(ns.xsd.integer), rdflib.RDFS.subClassOf, rdflib.URIRef(ns.bsfs.Number)), (rdflib.URIRef(ns.bse.comment), rdflib.RDFS.subClassOf, rdflib.URIRef(ns.bsfs.Predicate)), (rdflib.URIRef(ns.bse.filesize), rdflib.RDFS.subClassOf, rdflib.URIRef(ns.bsfs.Predicate)), @@ -351,12 +353,14 @@ class TestSparqlStore(unittest.TestCase): # instances of old classes were removed self.assertSetEqual(set(store._graph), { # schema hierarchy - (rdflib.URIRef(ns.bsfs.Feature), rdflib.RDFS.subClassOf, rdflib.URIRef(ns.bsfs.Predicate)), (rdflib.URIRef(ns.bsfs.Entity), rdflib.RDFS.subClassOf, rdflib.URIRef(ns.bsfs.Node)), (rdflib.URIRef(ns.bsfs.Tag), rdflib.RDFS.subClassOf, rdflib.URIRef(ns.bsfs.Node)), (rdflib.URIRef(ns.bsfs.User), rdflib.RDFS.subClassOf, rdflib.URIRef(ns.bsfs.Node)), (rdflib.URIRef(ns.xsd.boolean), rdflib.RDFS.subClassOf, rdflib.URIRef(ns.bsfs.Literal)), + (rdflib.URIRef(ns.bsfs.Array), rdflib.RDFS.subClassOf, rdflib.URIRef(ns.bsfs.Literal)), + (rdflib.URIRef(ns.bsfs.Feature), rdflib.RDFS.subClassOf, rdflib.URIRef(ns.bsfs.Array)), (rdflib.URIRef(ns.bsfs.Number), rdflib.RDFS.subClassOf, rdflib.URIRef(ns.bsfs.Literal)), + (rdflib.URIRef(ns.bsfs.Time), rdflib.RDFS.subClassOf, rdflib.URIRef(ns.bsfs.Literal)), (rdflib.URIRef(ns.xsd.integer), rdflib.RDFS.subClassOf, rdflib.URIRef(ns.bsfs.Number)), (rdflib.URIRef(ns.bse.shared), rdflib.RDFS.subClassOf, rdflib.URIRef(ns.bsfs.Predicate)), (rdflib.URIRef(ns.bse.tag), rdflib.RDFS.subClassOf, rdflib.URIRef(ns.bsfs.Predicate)), -- cgit v1.2.3 From 80a97bfa9f22d0d6dd25928fe1754a3a0d1de78a Mon Sep 17 00:00:00 2001 From: Matthias Baumgartner Date: Sun, 15 Jan 2023 21:00:12 +0100 Subject: Distance filter ast node --- bsfs/graph/resolve.py | 5 +++ bsfs/query/ast/filter_.py | 59 ++++++++++++++++++++------ bsfs/query/validator.py | 16 +++++++ bsfs/triple_store/sparql/distance.py | 56 ++++++++++++++++++++++++ bsfs/triple_store/sparql/parse_filter.py | 41 +++++++++++++++++- bsfs/triple_store/sparql/sparql.py | 13 +++++- test/graph/test_resolve.py | 13 ++++++ test/query/ast_test/test_filter_.py | 35 ++++++++++++++- test/query/test_validator.py | 27 ++++++++++++ test/triple_store/sparql/test_distance.py | 61 +++++++++++++++++++++++++++ test/triple_store/sparql/test_parse_filter.py | 50 ++++++++++++++++++++-- test/triple_store/sparql/test_sparql.py | 17 ++++++++ 12 files changed, 375 insertions(+), 18 deletions(-) create mode 100644 bsfs/triple_store/sparql/distance.py create mode 100644 test/triple_store/sparql/test_distance.py diff --git a/bsfs/graph/resolve.py b/bsfs/graph/resolve.py index b671204..00b778b 100644 --- a/bsfs/graph/resolve.py +++ b/bsfs/graph/resolve.py @@ -63,6 +63,8 @@ class Filter(): return self._and(type_, node) if isinstance(node, ast.filter.Or): return self._or(type_, node) + if isinstance(node, ast.filter.Distance): + return self._distance(type_, node) if isinstance(node, (ast.filter.Equals, ast.filter.Substring, \ ast.filter.StartsWith, ast.filter.EndsWith)): return self._value(type_, node) @@ -125,6 +127,9 @@ class Filter(): def _has(self, type_: bsc.Vertex, node: ast.filter.Has) -> ast.filter.Has: # pylint: disable=unused-argument return node + def _distance(self, type_: bsc.Vertex, node: ast.filter.Distance): # pylint: disable=unused-argument + return node + def _value(self, type_: bsc.Vertex, node: ast.filter._Value) -> ast.filter._Value: # pylint: disable=unused-argument return node diff --git a/bsfs/query/ast/filter_.py b/bsfs/query/ast/filter_.py index b129ded..2f0270c 100644 --- a/bsfs/query/ast/filter_.py +++ b/bsfs/query/ast/filter_.py @@ -252,8 +252,7 @@ class Has(FilterExpression): class _Value(FilterExpression): - """ - """ + """Matches some value.""" # target value. value: typing.Any @@ -277,13 +276,13 @@ class Is(_Value): class Equals(_Value): """Value matches exactly. - NOTE: Value format must correspond to literal type; can be a string, a number, or a Node + NOTE: Value must correspond to literal type. """ class Substring(_Value): """Value matches a substring - NOTE: value format must be a string + NOTE: value must be a string. """ @@ -295,9 +294,49 @@ class EndsWith(_Value): """Value ends with a given string.""" +class Distance(FilterExpression): + """Distance to a reference is (strictly) below a threshold. Assumes a Feature literal.""" + + # FIXME: + # (a) pass a node/predicate as anchor instead of a value. + # Then we don't need to materialize the reference. + # (b) pass a FilterExpression (_Bounded) instead of a threshold. + # Then, we could also query values greater than a threshold. + + # reference value. + reference: typing.Any + + # distance threshold. + threshold: float + + # closed (True) or open (False) bound. + strict: bool + + def __init__( + self, + reference: typing.Any, + threshold: float, + strict: bool = False, + ): + self.reference = reference + self.threshold = float(threshold) + self.strict = bool(strict) + + def __repr__(self) -> str: + return f'{typename(self)}({self.reference}, {self.threshold}, {self.strict})' + + def __hash__(self) -> int: + return hash((super().__hash__(), tuple(self.reference), self.threshold, self.strict)) + + def __eq__(self, other) -> bool: + return super().__eq__(other) \ + and self.reference == other.reference \ + and self.threshold == other.threshold \ + and self.strict == other.strict + + class _Bounded(FilterExpression): - """ - """ + """Value is bounded by a threshold. Assumes a Number literal.""" # bound. threshold: float @@ -327,15 +366,11 @@ class _Bounded(FilterExpression): class LessThan(_Bounded): - """Value is (strictly) smaller than threshold. - NOTE: only on numerical literals - """ + """Value is (strictly) smaller than threshold. Assumes a Number literal.""" class GreaterThan(_Bounded): - """Value is (strictly) larger than threshold - NOTE: only on numerical literals - """ + """Value is (strictly) larger than threshold. Assumes a Number literal.""" class Predicate(PredicateExpression): diff --git a/bsfs/query/validator.py b/bsfs/query/validator.py index ecea951..1b7f688 100644 --- a/bsfs/query/validator.py +++ b/bsfs/query/validator.py @@ -69,6 +69,8 @@ class Filter(): return self._not(type_, node) if isinstance(node, ast.filter.Has): return self._has(type_, node) + if isinstance(node, ast.filter.Distance): + return self._distance(type_, node) if isinstance(node, (ast.filter.Any, ast.filter.All)): return self._branch(type_, node) if isinstance(node, (ast.filter.And, ast.filter.Or)): @@ -177,6 +179,20 @@ class Filter(): # node.count is a numerical expression self._parse_filter_expression(self.schema.literal(ns.bsfs.Number), node.count) + def _distance(self, type_: bsc.Vertex, node: ast.filter.Distance): + # type is a Literal + if not isinstance(type_, bsc.Feature): + raise errors.ConsistencyError(f'expected a Feature, found {type_}') + # type exists in the schema + if type_ not in self.schema.literals(): + raise errors.ConsistencyError(f'literal {type_} is not in the schema') + # reference matches type_ + if len(node.reference) != type_.dimension: + raise errors.ConsistencyError(f'reference has dimension {len(node.reference)}, expected {type_.dimension}') + # FIXME: + #if node.reference.dtype != type_.dtype: + # raise errors.ConsistencyError(f'') + ## conditions diff --git a/bsfs/triple_store/sparql/distance.py b/bsfs/triple_store/sparql/distance.py new file mode 100644 index 0000000..2f5387a --- /dev/null +++ b/bsfs/triple_store/sparql/distance.py @@ -0,0 +1,56 @@ +""" + +Part of the BlackStar filesystem (bsfs) module. +A copy of the license is provided with the project. +Author: Matthias Baumgartner, 2022 +""" +# standard imports +import typing + +# external imports +import numpy as np + +# bsfs imports +from bsfs.namespace import ns + +# constants +EPS = 1e-9 + +# exports +__all__: typing.Sequence[str] = ( + 'DISTANCE_FU', + ) + + +## code ## + +def euclid(fst, snd) -> float: + """Euclidean distance (l2 norm).""" + fst = np.array(fst) + snd = np.array(snd) + return float(np.linalg.norm(fst - snd)) + +def cosine(fst, snd) -> float: + """Cosine distance.""" + fst = np.array(fst) + snd = np.array(snd) + if (fst == snd).all(): + return 0.0 + nrm0 = np.linalg.norm(fst) + nrm1 = np.linalg.norm(snd) + return float(1.0 - np.dot(fst, snd) / (nrm0 * nrm1 + EPS)) + +def manhatten(fst, snd) -> float: + """Manhatten (cityblock) distance (l1 norm).""" + fst = np.array(fst) + snd = np.array(snd) + return float(np.abs(fst - snd).sum()) + +# Known distance functions. +DISTANCE_FU = { + ns.bsfs.euclidean: euclid, + ns.bsfs.cosine: cosine, + ns.bsfs.manhatten: manhatten, +} + +## EOF ## diff --git a/bsfs/triple_store/sparql/parse_filter.py b/bsfs/triple_store/sparql/parse_filter.py index 5d8a2d9..8b6b976 100644 --- a/bsfs/triple_store/sparql/parse_filter.py +++ b/bsfs/triple_store/sparql/parse_filter.py @@ -5,19 +5,29 @@ A copy of the license is provided with the project. Author: Matthias Baumgartner, 2022 """ # imports +import operator import typing +# external imports +import rdflib + # bsfs imports from bsfs import schema as bsc from bsfs.namespace import ns from bsfs.query import ast from bsfs.utils import URI, errors +# inner-module imports +from .distance import DISTANCE_FU + # exports __all__: typing.Sequence[str] = ( 'Filter', ) + +## code ## + class _GenHopName(): """Generator that produces a new unique symbol name with each iteration.""" @@ -46,7 +56,8 @@ class Filter(): # Generator that produces unique symbol names. ngen: _GenHopName - def __init__(self, schema): + def __init__(self, graph, schema): + self.graph = graph self.schema = schema self.ngen = _GenHopName() @@ -84,6 +95,8 @@ class Filter(): return self._not(type_, node, head) if isinstance(node, ast.filter.Has): return self._has(type_, node, head) + if isinstance(node, ast.filter.Distance): + return self._distance(type_, node, head) if isinstance(node, ast.filter.Any): return self._any(type_, node, head) if isinstance(node, ast.filter.All): @@ -243,6 +256,32 @@ class Filter(): # combine return num_preds + ' . ' + count_bounds + def _distance(self, node_type: bsc.Vertex, node: ast.filter.Distance, head: str) -> str: + """ + """ + if not isinstance(node_type, bsc.Feature): + raise errors.BackendError(f'expected Feature, found {node_type}') + if len(node.reference) != node_type.dimension: + raise errors.ConsistencyError( + f'reference has dimension {len(node.reference)}, expected {node_type.dimension}') + # get distance metric + dist = DISTANCE_FU[node_type.distance] + # get operator + cmp = operator.lt if node.strict else operator.le + # get candidate values + candidates = { + f'"{cand}"^^<{node_type.uri}>' + for cand + in self.graph.objects() + if isinstance(cand, rdflib.Literal) + and cand.datatype == rdflib.URIRef(node_type.uri) + and cmp(dist(cand.value, node.reference), node.threshold) + } + # combine candidate values + values = ' '.join(candidates) if len(candidates) else f'"impossible value"^^<{ns.xsd.string}>' + # return sparql fragment + return f'VALUES {head} {{ {values} }}' + def _is(self, node_type: bsc.Vertex, node: ast.filter.Is, head: str) -> str: """ """ diff --git a/bsfs/triple_store/sparql/sparql.py b/bsfs/triple_store/sparql/sparql.py index 3877d1a..dfd9871 100644 --- a/bsfs/triple_store/sparql/sparql.py +++ b/bsfs/triple_store/sparql/sparql.py @@ -18,6 +18,7 @@ from bsfs.utils import errors, URI # inner-module imports from . import parse_filter from .. import base +from .distance import DISTANCE_FU # exports @@ -97,7 +98,7 @@ class SparqlStore(base.TripleStoreBase): self._transaction = _Transaction(self._graph) # NOTE: parsing bsfs.query.ast.filter.Has requires xsd:integer. self._schema = bsc.Schema(literals={bsc.ROOT_NUMBER.child(ns.xsd.integer)}) - self._filter_parser = parse_filter.Filter(self._schema) + self._filter_parser = parse_filter.Filter(self._graph, self._schema) # NOTE: mypy and pylint complain about the **kwargs not being listed (contrasting super) # However, not having it here is clearer since it's explicit that there are no arguments. @@ -123,6 +124,16 @@ class SparqlStore(base.TripleStoreBase): # check compatibility: No contradicting definitions if not self.schema.consistent_with(schema): raise errors.ConsistencyError(f'{schema} is inconsistent with {self.schema}') + # check distance functions of features + invalid = { + (cand.uri, cand.distance) + for cand + in schema.literals() + if isinstance(cand, bsc.Feature) and cand.distance not in DISTANCE_FU} + if len(invalid) > 0: + cand, dist = zip(*invalid) + raise ValueError( + f'unknown distance function {",".join(dist)} in feature {", ".join(cand)}') # commit the current transaction self.commit() diff --git a/test/graph/test_resolve.py b/test/graph/test_resolve.py index 0861a53..0918b02 100644 --- a/test/graph/test_resolve.py +++ b/test/graph/test_resolve.py @@ -46,6 +46,13 @@ class TestFilter(unittest.TestCase): bsfs:Feature rdfs:subClassOf bsfs:Array . xsd:integer rdfs:subClassOf bsfs:Number . + bsfs:Colors rdfs:subClassOf bsfs:Feature ; + bsfs:dimension "5"^^xsd:integer . + + bse:colors rdfs:subClassOf bsfs:Predicate ; + rdfs:domain bsfs:Entity ; + rdfs:range bsfs:Colors . + bse:comment rdfs:subClassOf bsfs:Predicate ; rdfs:domain bsfs:Entity ; rdfs:range xsd:string ; @@ -147,12 +154,18 @@ class TestFilter(unittest.TestCase): self.assertEqual(resolver(schema.node(ns.bsfs.Entity), ast.filter.Has(ns.bse.comment)), ast.filter.Has(ns.bse.comment)) + # for sake of completeness: Distance + self.assertEqual(resolver(schema.node(ns.bsfs.Entity), + ast.filter.Any(ns.bse.colors, ast.filter.Distance([1,2,3,4,5], 1))), + ast.filter.Any(ns.bse.colors, ast.filter.Distance([1,2,3,4,5], 1))) # route errors self.assertRaises(errors.BackendError, resolver, schema.node(ns.bsfs.Tag), ast.filter.Predicate(ns.bse.comment)) self.assertRaises(errors.BackendError, resolver, schema.node(ns.bsfs.Tag), ast.filter.Any(ast.filter.PredicateExpression(), ast.filter.Equals('foo'))) self.assertRaises(errors.BackendError, resolver._one_of, ast.filter.OneOf(ast.filter.Predicate(ns.bsfs.Predicate))) + # for sake of coverage completeness: valid OneOf + self.assertIsNotNone(resolver._one_of(ast.filter.OneOf(ast.filter.Predicate(ns.bse.colors)))) # check schema consistency self.assertRaises(errors.ConsistencyError, resolver, schema.node(ns.bsfs.Tag), diff --git a/test/query/ast_test/test_filter_.py b/test/query/ast_test/test_filter_.py index 4f69bdc..9eb92e2 100644 --- a/test/query/ast_test/test_filter_.py +++ b/test/query/ast_test/test_filter_.py @@ -15,7 +15,7 @@ from bsfs.utils import URI from bsfs.query.ast.filter_ import _Expression, FilterExpression, PredicateExpression from bsfs.query.ast.filter_ import _Branch, Any, All from bsfs.query.ast.filter_ import _Agg, And, Or -from bsfs.query.ast.filter_ import Not, Has +from bsfs.query.ast.filter_ import Not, Has, Distance from bsfs.query.ast.filter_ import _Value, Is, Equals, Substring, StartsWith, EndsWith from bsfs.query.ast.filter_ import _Bounded, LessThan, GreaterThan from bsfs.query.ast.filter_ import Predicate, OneOf @@ -284,6 +284,39 @@ class TestValue(unittest.TestCase): self.assertEqual(cls(f).value, f) +class TestDistance(unittest.TestCase): + def test_essentials(self): + ref = (1,2,3) + # comparison + self.assertEqual(Distance(ref, 3), Distance(ref, 3)) + self.assertEqual(hash(Distance(ref, 3)), hash(Distance(ref, 3))) + # comparison respects type + self.assertNotEqual(Distance(ref, 3), FilterExpression()) + self.assertNotEqual(hash(Distance(ref, 3)), hash(FilterExpression())) + # comparison respects reference + self.assertNotEqual(Distance((1,2,3), 3, False), Distance((1,2), 3, False)) + self.assertNotEqual(hash(Distance((1,2,3), 3, False)), hash(Distance((1,2), 3, False))) + self.assertNotEqual(Distance((1,2,3), 3, False), Distance((1,5,3), 3, False)) + self.assertNotEqual(hash(Distance((1,2,3), 3, False)), hash(Distance((1,5,3), 3, False))) + # comparison respects threshold + self.assertNotEqual(Distance((1,2,3), 3, False), Distance((1,2,3), 3.1, False)) + self.assertNotEqual(hash(Distance((1,2,3), 3, False)), hash(Distance((1,2,3), 3.1, False))) + # comparison respects strict flag + self.assertNotEqual(Distance((1,2,3), 3, False), Distance((1,2,3), 3, True)) + self.assertNotEqual(hash(Distance((1,2,3), 3, False)), hash(Distance((1,2,3), 3, True))) + # string conversion + self.assertEqual(str(Distance(ref, 3, False)), 'Distance((1, 2, 3), 3.0, False)') + self.assertEqual(repr(Distance(ref, 3, False)), 'Distance((1, 2, 3), 3.0, False)') + + def test_members(self): + self.assertEqual(Distance((1,2,3), 3, False).reference, (1,2,3)) + self.assertEqual(Distance((3,2,1), 3, False).reference, (3,2,1)) + self.assertEqual(Distance((1,2,3), 3, False).threshold, 3.0) + self.assertEqual(Distance((1,2,3), 53.45, False).threshold, 53.45) + self.assertEqual(Distance((1,2,3), 3, False).strict, False) + self.assertEqual(Distance((1,2,3), 3, True).strict, True) + + class TestBounded(unittest.TestCase): def test_essentials(self): # comparison respects type diff --git a/test/query/test_validator.py b/test/query/test_validator.py index 63ead52..dc9d913 100644 --- a/test/query/test_validator.py +++ b/test/query/test_validator.py @@ -38,6 +38,15 @@ class TestFilter(unittest.TestCase): bsfs:Feature rdfs:subClassOf bsfs:Array . xsd:integer rdfs:subClassOf bsfs:Number . + bsfs:Colors rdfs:subClassOf bsfs:Feature ; + bsfs:dimension "5"^^xsd:integer ; + bsfs:dtype bsfs:f32 . + + bse:color rdfs:subClassOf bsfs:Predicate ; + rdfs:domain bsfs:Node ; + rdfs:range bsfs:Colors ; + bsfs:unique "true"^^xsd:boolean . + bse:comment rdfs:subClassOf bsfs:Predicate ; rdfs:domain bsfs:Node ; rdfs:range xsd:string ; @@ -88,6 +97,7 @@ class TestFilter(unittest.TestCase): ), ast.filter.Not(ast.filter.Any(ns.bse.comment, ast.filter.Not(ast.filter.Equals('hello world')))), + ast.filter.Any(ns.bse.color, ast.filter.Distance([1,2,3,4,5], 3)), ))))) # invalid paths raise consistency error self.assertRaises(errors.ConsistencyError, self.validate, self.schema.node(ns.bsfs.Entity), @@ -257,6 +267,23 @@ class TestFilter(unittest.TestCase): self.assertIsNone(self.validate._bounded(self.schema.literal(ns.xsd.integer), ast.filter.LessThan(0))) self.assertIsNone(self.validate._bounded(self.schema.literal(ns.xsd.integer), ast.filter.GreaterThan(0))) + def test_distance(self): + # type must be a literal + self.assertRaises(errors.ConsistencyError, self.validate._distance, self.schema.node(ns.bsfs.Node), + ast.filter.Distance([1,2,3], 1, False)) + # type must be a feature + self.assertRaises(errors.ConsistencyError, self.validate._distance, self.schema.literal(ns.bsfs.Array), + ast.filter.Distance([1,2,3], 1, False)) + # type must be in the schema + self.assertRaises(errors.ConsistencyError, self.validate._distance, self.schema.literal(ns.bsfs.Feature).child(ns.bsfs.Invalid), + ast.filter.Distance([1,2,3], 1, False)) + # FIXME: reference must be a numpy array + # reference must have the correct dimension + self.assertRaises(errors.ConsistencyError, self.validate._distance, self.schema.literal(ns.bsfs.Colors), + ast.filter.Distance([1,2,3], 1, False)) + # FIXME: reference must have the correct dtype + # distance accepts correct expressions + self.assertIsNone(self.validate._distance(self.schema.literal(ns.bsfs.Colors), ast.filter.Distance([1,2,3,4,5], 1, False))) ## main ## diff --git a/test/triple_store/sparql/test_distance.py b/test/triple_store/sparql/test_distance.py new file mode 100644 index 0000000..0659459 --- /dev/null +++ b/test/triple_store/sparql/test_distance.py @@ -0,0 +1,61 @@ +""" + +Part of the bsfs test suite. +A copy of the license is provided with the project. +Author: Matthias Baumgartner, 2022 +""" +# imports +import numpy as np +import unittest + +# objects to test +from bsfs.triple_store.sparql import distance + + +## code ## + +class TestDistance(unittest.TestCase): + + def test_euclid(self): + # self-distance is zero + self.assertEqual(distance.euclid([1,2,3,4], [1,2,3,4]), 0.0) + # accepts list-like arguments + self.assertAlmostEqual(distance.euclid([1,2,3,4], [2,3,4,5]), 2.0, 3) + self.assertAlmostEqual(distance.euclid((1,2,3,4), (2,3,4,5)), 2.0, 3) + # dimension can vary + self.assertAlmostEqual(distance.euclid([1,2,3], [2,3,4]), 1.732, 3) + self.assertAlmostEqual(distance.euclid([1,2,3,4,5], [2,3,4,5,6]), 2.236, 3) + # vector can be zero + self.assertAlmostEqual(distance.euclid([0,0,0], [1,2,3]), 3.742, 3) + + def test_cosine(self): + # self-distance is zero + self.assertEqual(distance.cosine([1,2,3,4], [1,2,3,4]), 0.0) + # accepts list-like arguments + self.assertAlmostEqual(distance.cosine([1,2,3,4], [4,3,2,1]), 0.333, 3) + self.assertAlmostEqual(distance.cosine((1,2,3,4), (4,3,2,1)), 0.333, 3) + # dimension can vary + self.assertAlmostEqual(distance.cosine([1,2,3], [3,2,1]), 0.286, 3) + self.assertAlmostEqual(distance.cosine([1,2,3,4,5], [5,4,3,2,1]), 0.364, 3) + # vector can be zero + self.assertAlmostEqual(distance.cosine([0,0,0], [1,2,3]), 1.0, 3) + + def test_manhatten(self): + # self-distance is zero + self.assertEqual(distance.manhatten([1,2,3,4], [1,2,3,4]), 0.0) + # accepts list-like arguments + self.assertAlmostEqual(distance.manhatten([1,2,3,4], [2,3,4,5]), 4.0, 3) + self.assertAlmostEqual(distance.manhatten((1,2,3,4), (2,3,4,5)), 4.0, 3) + # dimension can vary + self.assertAlmostEqual(distance.manhatten([1,2,3], [2,3,4]), 3.0, 3) + self.assertAlmostEqual(distance.manhatten([1,2,3,4,5], [2,3,4,5,6]), 5.0, 3) + # vector can be zero + self.assertAlmostEqual(distance.manhatten([0,0,0], [1,2,3]), 6.0, 3) + + +## main ## + +if __name__ == '__main__': + unittest.main() + +## EOF ## diff --git a/test/triple_store/sparql/test_parse_filter.py b/test/triple_store/sparql/test_parse_filter.py index 5c16f11..8764535 100644 --- a/test/triple_store/sparql/test_parse_filter.py +++ b/test/triple_store/sparql/test_parse_filter.py @@ -42,6 +42,15 @@ class TestParseFilter(unittest.TestCase): xsd:integer rdfs:subClassOf bsfs:Number . bsfs:URI rdfs:subClassOf bsfs:Literal . + bsfs:Colors rdfs:subClassOf bsfs:Feature ; + bsfs:dimension "4"^^xsd:integer ; + bsfs:dtype xsd:integer ; + bsfs:distance bsfs:euclidean . + + bse:colors rdfs:subClassOf bsfs:Predicate ; + rdfs:domain bsfs:Entity ; + rdfs:range bsfs:Colors . + bse:comment rdfs:subClassOf bsfs:Predicate ; rdfs:domain bsfs:Node ; rdfs:range xsd:string ; @@ -74,9 +83,6 @@ class TestParseFilter(unittest.TestCase): ''') - # parser instance - self.parser = Filter(self.schema) - # graph to test queries self.graph = rdflib.Graph() # schema hierarchies @@ -117,6 +123,13 @@ class TestParseFilter(unittest.TestCase): # image iso self.graph.add((rdflib.URIRef('http://example.com/image#1234'), rdflib.URIRef(ns.bse.iso), rdflib.Literal(1234, datatype=rdflib.XSD.integer))) self.graph.add((rdflib.URIRef('http://example.com/image#4321'), rdflib.URIRef(ns.bse.iso), rdflib.Literal(4321, datatype=rdflib.XSD.integer))) + # color features + self.graph.add((rdflib.URIRef('http://example.com/entity#1234'), rdflib.URIRef(ns.bse.colors), rdflib.Literal([1,2,3,4], datatype=rdflib.URIRef(ns.bsfs.Colors)))) + self.graph.add((rdflib.URIRef('http://example.com/entity#4321'), rdflib.URIRef(ns.bse.colors), rdflib.Literal([4,3,2,1], datatype=rdflib.URIRef(ns.bsfs.Colors)))) + self.graph.add((rdflib.URIRef('http://example.com/image#1234'), rdflib.URIRef(ns.bse.colors), rdflib.Literal([3,4,2,1], datatype=rdflib.URIRef(ns.bsfs.Colors)))) + + # parser instance + self.parser = Filter(self.graph, self.schema) def test_routing(self): @@ -617,6 +630,37 @@ class TestParseFilter(unittest.TestCase): {'http://example.com/tag#1234'}) + def test_distance(self): + # node colors distance to [2,4,3,1] + # entity#1234 [1,2,3,4] 3.742 + # entity#4321 [4,3,2,1] 2.449 + # image#1234 [3,4,2,1] 1.414 + + # _distance expects a feature + self.assertRaises(errors.BackendError, self.parser._distance, self.schema.node(ns.bsfs.Entity), ast.filter.Distance([1,2,3,4], 1), '') + # reference must have the correct dimension + self.assertRaises(errors.ConsistencyError, self.parser._distance, self.schema.literal(ns.bsfs.Colors), ast.filter.Distance([1,2,3], 1), '') + self.assertRaises(errors.ConsistencyError, self.parser._distance, self.schema.literal(ns.bsfs.Colors), ast.filter.Distance([1,2,3,4,5], 1), '') + # _distance respects threshold + q = self.parser(self.schema.node(ns.bsfs.Entity), ast.filter.Any(ns.bse.colors, ast.filter.Distance([2,4,3,1], 4))) + self.assertSetEqual({str(guid) for guid, in self.graph.query(q)}, + {'http://example.com/entity#1234', 'http://example.com/entity#4321', 'http://example.com/image#1234'}) + q = self.parser(self.schema.node(ns.bsfs.Entity), ast.filter.Any(ns.bse.colors, ast.filter.Distance([2,4,3,1], 3))) + self.assertSetEqual({str(guid) for guid, in self.graph.query(q)}, + {'http://example.com/entity#4321', 'http://example.com/image#1234'}) + q = self.parser(self.schema.node(ns.bsfs.Entity), ast.filter.Any(ns.bse.colors, ast.filter.Distance([2,4,3,1], 2))) + self.assertSetEqual({str(guid) for guid, in self.graph.query(q)}, + {'http://example.com/image#1234'}) + # result set can be empty + q = self.parser(self.schema.node(ns.bsfs.Entity), ast.filter.Any(ns.bse.colors, ast.filter.Distance([2,4,3,1], 1))) + self.assertSetEqual({str(guid) for guid, in self.graph.query(q)}, set()) + # _distance respects strict + q = self.parser(self.schema.node(ns.bsfs.Entity), ast.filter.Any(ns.bse.colors, ast.filter.Distance([1,2,3,4], 0, False))) + self.assertSetEqual({str(guid) for guid, in self.graph.query(q)}, + {'http://example.com/entity#1234'}) + q = self.parser(self.schema.node(ns.bsfs.Entity), ast.filter.Any(ns.bse.colors, ast.filter.Distance([1,2,3,4], 0, True))) + self.assertSetEqual({str(guid) for guid, in self.graph.query(q)}, set()) + def test_one_of(self): # _one_of expects a node self.assertRaises(errors.BackendError, self.parser._one_of, diff --git a/test/triple_store/sparql/test_sparql.py b/test/triple_store/sparql/test_sparql.py index 1f56a7e..435ca28 100644 --- a/test/triple_store/sparql/test_sparql.py +++ b/test/triple_store/sparql/test_sparql.py @@ -392,6 +392,23 @@ class TestSparqlStore(unittest.TestCase): class Foo(): pass self.assertRaises(TypeError, setattr, store, 'schema', Foo()) + # cannot define features w/o known distance function + invalid = bsc.from_string(''' + prefix rdfs: + prefix xsd: + prefix bsfs: + prefix bse: + + bsfs:Array rdfs:subClassOf bsfs:Literal . + bsfs:Feature rdfs:subClassOf bsfs:Array . + + bsfs:Colors rdfs:subClassOf bsfs:Feature ; + bsfs:dimension "4"^^xsd:integer ; + bsfs:distance bsfs:foobar . + + ''') + self.assertRaises(ValueError, setattr, store, 'schema', invalid) + # cannot migrate to incompatible schema invalid = bsc.from_string(''' prefix rdfs: -- cgit v1.2.3 From afdfc25408c3b9d2c779c83e2e193d68a973810b Mon Sep 17 00:00:00 2001 From: Matthias Baumgartner Date: Mon, 16 Jan 2023 21:38:55 +0100 Subject: namespace to string plain uri --- bsfs/namespace/namespace.py | 2 +- test/namespace/test_namespace.py | 12 ++++++------ 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/bsfs/namespace/namespace.py b/bsfs/namespace/namespace.py index f652dcd..1d443c1 100644 --- a/bsfs/namespace/namespace.py +++ b/bsfs/namespace/namespace.py @@ -59,7 +59,7 @@ class Namespace(): return hash((type(self), self.prefix, self.fsep, self.psep)) def __str__(self) -> str: - return f'{typename(self)}({self.prefix})' + return str(self.prefix) def __repr__(self) -> str: return f'{typename(self)}({self.prefix}, {self.fsep}, {self.psep})' diff --git a/test/namespace/test_namespace.py b/test/namespace/test_namespace.py index f109653..2536203 100644 --- a/test/namespace/test_namespace.py +++ b/test/namespace/test_namespace.py @@ -20,15 +20,15 @@ from bsfs.namespace.namespace import Namespace, ClosedNamespace class TestNamespace(unittest.TestCase): def test_essentials(self): # string conversion - self.assertEqual(str(Namespace('http://example.org/')), 'Namespace(http://example.org)') - self.assertEqual(str(Namespace('http://example.org#')), 'Namespace(http://example.org)') + self.assertEqual(str(Namespace('http://example.org/')), 'http://example.org') + self.assertEqual(str(Namespace('http://example.org#')), 'http://example.org') self.assertEqual(repr(Namespace('http://example.org/')), 'Namespace(http://example.org, #, /)') self.assertEqual(repr(Namespace('http://example.org#')), 'Namespace(http://example.org, #, /)') self.assertEqual(repr(Namespace('http://example.org', fsep='.')), 'Namespace(http://example.org, ., /)') self.assertEqual(repr(Namespace('http://example.org', psep='.')), 'Namespace(http://example.org, #, .)') # repeated separators are truncated - self.assertEqual(str(Namespace('http://example.org////')), 'Namespace(http://example.org)') - self.assertEqual(str(Namespace('http://example.org####')), 'Namespace(http://example.org)') + self.assertEqual(str(Namespace('http://example.org////')), 'http://example.org') + self.assertEqual(str(Namespace('http://example.org####')), 'http://example.org') self.assertEqual(repr(Namespace('http://example.org///##')), 'Namespace(http://example.org, #, /)') # comparison class Foo(Namespace): pass @@ -83,8 +83,8 @@ class TestNamespace(unittest.TestCase): class TestClosedNamespace(unittest.TestCase): def test_essentials(self): # string conversion - self.assertEqual(str(ClosedNamespace('http://example.org/')), 'ClosedNamespace(http://example.org)') - self.assertEqual(str(ClosedNamespace('http://example.org#')), 'ClosedNamespace(http://example.org)') + self.assertEqual(str(ClosedNamespace('http://example.org/')), 'http://example.org') + self.assertEqual(str(ClosedNamespace('http://example.org#')), 'http://example.org') self.assertEqual(repr(ClosedNamespace('http://example.org/')), 'ClosedNamespace(http://example.org, #, /)') self.assertEqual(repr(ClosedNamespace('http://example.org#')), 'ClosedNamespace(http://example.org, #, /)') self.assertEqual(repr(ClosedNamespace('http://example.org', fsep='.')), 'ClosedNamespace(http://example.org, ., /)') -- cgit v1.2.3 From 76fa694911f54e293ddf517246c6c4a1e8e745fd Mon Sep 17 00:00:00 2001 From: Matthias Baumgartner Date: Mon, 16 Jan 2023 21:39:47 +0100 Subject: uuid from dict --- bsfs/utils/uuid.py | 7 +++++++ test/utils/test_uuid.py | 4 ++++ 2 files changed, 11 insertions(+) diff --git a/bsfs/utils/uuid.py b/bsfs/utils/uuid.py index 6366b18..ba5cf52 100644 --- a/bsfs/utils/uuid.py +++ b/bsfs/utils/uuid.py @@ -7,6 +7,7 @@ Author: Matthias Baumgartner, 2022 # imports from collections import abc import hashlib +import json import os import platform import random @@ -105,4 +106,10 @@ class UCID(): with open(path, 'rb') as ifile: return HASH(ifile.read()).hexdigest() + + @staticmethod + def from_dict(content: dict) -> str: + """Get the content from a dict.""" + return HASH(json.dumps(content).encode('ascii', 'ignore')).hexdigest() + ## EOF ## diff --git a/test/utils/test_uuid.py b/test/utils/test_uuid.py index 49176d4..0de96ed 100644 --- a/test/utils/test_uuid.py +++ b/test/utils/test_uuid.py @@ -83,6 +83,10 @@ class TestUCID(unittest.TestCase): def test_from_path(self): self.assertEqual(UCID.from_path(self._path), self._checksum) + def test_from_dict(self): + self.assertEqual(UCID.from_dict({'hello': 'world', 'foo': 1234, 'bar': False}), + '8d2544395a0d2827e3d9ce8cd619d5e3f801e8126bf3f93ee5abd38158959585') + ## main ## -- cgit v1.2.3 From 3504609e1ba1f7f653fa79910474bebd3ec24d8a Mon Sep 17 00:00:00 2001 From: Matthias Baumgartner Date: Mon, 16 Jan 2023 21:41:20 +0100 Subject: various minor fixes --- bsfs/query/validator.py | 4 +--- bsfs/schema/serialize.py | 18 +++++++++++++----- bsfs/triple_store/sparql/sparql.py | 2 +- bsfs/utils/errors.py | 3 +++ test/triple_store/sparql/test_sparql.py | 2 +- 5 files changed, 19 insertions(+), 10 deletions(-) diff --git a/bsfs/query/validator.py b/bsfs/query/validator.py index 1b7f688..904ac14 100644 --- a/bsfs/query/validator.py +++ b/bsfs/query/validator.py @@ -189,9 +189,7 @@ class Filter(): # reference matches type_ if len(node.reference) != type_.dimension: raise errors.ConsistencyError(f'reference has dimension {len(node.reference)}, expected {type_.dimension}') - # FIXME: - #if node.reference.dtype != type_.dtype: - # raise errors.ConsistencyError(f'') + # FIXME: test dtype ## conditions diff --git a/bsfs/schema/serialize.py b/bsfs/schema/serialize.py index 8b31737..acc009a 100644 --- a/bsfs/schema/serialize.py +++ b/bsfs/schema/serialize.py @@ -35,8 +35,11 @@ def from_string(schema_str: str) -> schema.Schema: graph.parse(data=schema_str, format='turtle') # helper functions - # FIXME: type annotation - def _fetch_value(subject: URI, predicate: rdflib.URIRef, value_factory) -> typing.Optional[typing.Any]: + def _fetch_value( + subject: URI, + predicate: rdflib.URIRef, + value_factory: typing.Callable[[typing.Any], typing.Any], + ) -> typing.Optional[typing.Any]: """Fetch the object of a given subject and predicate. Raises a `errors.ConsistencyError` if multiple objects match. """ @@ -242,9 +245,14 @@ def to_string(schema_inst: schema.Schema, fmt: str = 'turtle') -> str: for triple in _parse(node): graph.add(triple) # add known namespaces for readability - # FIXME: more systematically (e.g. for all in ns?) - graph.bind('bsfs', rdflib.URIRef('http://bsfs.ai/schema/')) - graph.bind('bse', rdflib.URIRef('http://bsfs.ai/schema/Entity#')) + # FIXME: more generically? + graph.bind('bse', rdflib.URIRef(ns.bse[''])) + graph.bind('bsfs', rdflib.URIRef(ns.bsfs[''])) + graph.bind('bsm', rdflib.URIRef(ns.bsm[''])) + graph.bind('rdf', rdflib.URIRef(ns.rdf[''])) + graph.bind('rdfs', rdflib.URIRef(ns.rdfs[''])) + graph.bind('schema', rdflib.URIRef(ns.schema[''])) + graph.bind('xsd', rdflib.URIRef(ns.xsd[''])) # serialize to turtle return graph.serialize(format=fmt) diff --git a/bsfs/triple_store/sparql/sparql.py b/bsfs/triple_store/sparql/sparql.py index dfd9871..fedd227 100644 --- a/bsfs/triple_store/sparql/sparql.py +++ b/bsfs/triple_store/sparql/sparql.py @@ -132,7 +132,7 @@ class SparqlStore(base.TripleStoreBase): if isinstance(cand, bsc.Feature) and cand.distance not in DISTANCE_FU} if len(invalid) > 0: cand, dist = zip(*invalid) - raise ValueError( + raise errors.UnsupportedError( f'unknown distance function {",".join(dist)} in feature {", ".join(cand)}') # commit the current transaction diff --git a/bsfs/utils/errors.py b/bsfs/utils/errors.py index be9d40e..6ae6484 100644 --- a/bsfs/utils/errors.py +++ b/bsfs/utils/errors.py @@ -41,4 +41,7 @@ class ConfigError(_BSFSError): class BackendError(_BSFSError): """Could not parse an AST structure.""" +class UnsupportedError(_BSFSError): + """Some requested functionality is not supported by an implementation.""" + ## EOF ## diff --git a/test/triple_store/sparql/test_sparql.py b/test/triple_store/sparql/test_sparql.py index 435ca28..7fbfb65 100644 --- a/test/triple_store/sparql/test_sparql.py +++ b/test/triple_store/sparql/test_sparql.py @@ -407,7 +407,7 @@ class TestSparqlStore(unittest.TestCase): bsfs:distance bsfs:foobar . ''') - self.assertRaises(ValueError, setattr, store, 'schema', invalid) + self.assertRaises(errors.UnsupportedError, setattr, store, 'schema', invalid) # cannot migrate to incompatible schema invalid = bsc.from_string(''' -- cgit v1.2.3