diff options
author | Matthias Baumgartner <dev@igsor.net> | 2023-01-11 21:20:47 +0100 |
---|---|---|
committer | Matthias Baumgartner <dev@igsor.net> | 2023-01-11 21:20:47 +0100 |
commit | c664d19e7d4a0aa0762c30a72ae238cf818891ab (patch) | |
tree | 93349de711a18cff8329745af22710738b933cdc | |
parent | 7f5a2920ef311b2077300714d7700313077a0bf6 (diff) | |
download | bsfs-c664d19e7d4a0aa0762c30a72ae238cf818891ab.tar.gz bsfs-c664d19e7d4a0aa0762c30a72ae238cf818891ab.tar.bz2 bsfs-c664d19e7d4a0aa0762c30a72ae238cf818891ab.zip |
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
-rw-r--r-- | bsfs/schema/__init__.py | 5 | ||||
-rw-r--r-- | bsfs/schema/schema.py | 111 | ||||
-rw-r--r-- | bsfs/schema/serialize.py | 143 | ||||
-rw-r--r-- | bsfs/schema/types.py | 183 | ||||
-rw-r--r-- | test/schema/test_schema.py | 271 | ||||
-rw-r--r-- | test/schema/test_serialize.py | 1007 | ||||
-rw-r--r-- | test/schema/test_types.py | 240 |
7 files changed, 1578 insertions, 382 deletions
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: <http://www.w3.org/2000/01/rdf-schema#> - prefix xsd: <http://www.w3.org/2001/XMLSchema#> - prefix bsfs: <http://bsfs.ai/schema/> - prefix bse: <http://bsfs.ai/schema/Entity#> - - 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: <http://www.w3.org/2000/01/rdf-schema#> - prefix xsd: <http://www.w3.org/2001/XMLSchema#> - prefix bsfs: <http://bsfs.ai/schema/> - prefix bse: <http://bsfs.ai/schema/Entity#> - - 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: <http://www.w3.org/2000/01/rdf-schema#> - prefix xsd: <http://www.w3.org/2001/XMLSchema#> - prefix bsfs: <http://bsfs.ai/schema/> - prefix bse: <http://bsfs.ai/schema/Entity#> - - 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: <http://www.w3.org/2000/01/rdf-schema#> - prefix bsfs: <http://bsfs.ai/schema/> - 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: <http://www.w3.org/2000/01/rdf-schema#> - prefix xsd: <http://www.w3.org/2001/XMLSchema#> - prefix bsfs: <http://bsfs.ai/schema/> - prefix bse: <http://bsfs.ai/schema/Entity#> - - 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: <http://www.w3.org/2000/01/rdf-schema#> - prefix xsd: <http://www.w3.org/2001/XMLSchema#> - prefix bsfs: <http://bsfs.ai/schema/> - prefix bse: <http://bsfs.ai/schema/Entity#> - - 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: <http://www.w3.org/2000/01/rdf-schema#> - prefix xsd: <http://www.w3.org/2001/XMLSchema#> - prefix bsfs: <http://bsfs.ai/schema/> - prefix bse: <http://bsfs.ai/schema/Entity#> - - 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: <http://www.w3.org/2000/01/rdf-schema#> - prefix xsd: <http://www.w3.org/2001/XMLSchema#> - prefix bsfs: <http://bsfs.ai/schema/> - - 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: <http://www.w3.org/2000/01/rdf-schema#> - prefix xsd: <http://www.w3.org/2001/XMLSchema#> - prefix bsfs: <http://bsfs.ai/schema/> - - 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: <http://www.w3.org/2000/01/rdf-schema#> - prefix xsd: <http://www.w3.org/2001/XMLSchema#> - prefix bsfs: <http://bsfs.ai/schema/> - prefix bse: <http://bsfs.ai/schema/Entity#> - - 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: <http://www.w3.org/2000/01/rdf-schema#> - prefix xsd: <http://www.w3.org/2001/XMLSchema#> - prefix bsfs: <http://bsfs.ai/schema/> - prefix bse: <http://bsfs.ai/schema/Entity#> - - 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: <http://www.w3.org/2000/01/rdf-schema#> - prefix xsd: <http://www.w3.org/2001/XMLSchema#> - prefix bsfs: <http://bsfs.ai/schema/> - prefix bse: <http://bsfs.ai/schema/Entity#> - - 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: <http://www.w3.org/2000/01/rdf-schema#> + prefix bsfs: <http://bsfs.ai/schema/> + 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: <http://www.w3.org/2000/01/rdf-schema#> + prefix xsd: <http://www.w3.org/2001/XMLSchema#> + prefix bsfs: <http://bsfs.ai/schema/> + prefix bse: <http://bsfs.ai/schema/Entity#> + + 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: <http://www.w3.org/2000/01/rdf-schema#> + prefix xsd: <http://www.w3.org/2001/XMLSchema#> + prefix bsfs: <http://bsfs.ai/schema/> + + 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: <http://www.w3.org/2000/01/rdf-schema#> + prefix xsd: <http://www.w3.org/2001/XMLSchema#> + prefix bsfs: <http://bsfs.ai/schema/> + prefix bse: <http://bsfs.ai/schema/Entity#> + + 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: <http://www.w3.org/2000/01/rdf-schema#> + prefix xsd: <http://www.w3.org/2001/XMLSchema#> + prefix bsfs: <http://bsfs.ai/schema/> + prefix bse: <http://bsfs.ai/schema/Entity#> + + # 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: <http://www.w3.org/2000/01/rdf-schema#> + prefix xsd: <http://www.w3.org/2001/XMLSchema#> + prefix bsfs: <http://bsfs.ai/schema/> + prefix bse: <http://bsfs.ai/schema/Entity#> + + 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: <http://www.w3.org/2000/01/rdf-schema#> + prefix xsd: <http://www.w3.org/2001/XMLSchema#> + prefix bsfs: <http://bsfs.ai/schema/> + + bsfs:Entity rdfs:subClassOf bsfs:Node . + + ''').node(ns.bsfs.Entity).annotations, {}) + self.assertDictEqual(from_string(''' + prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> + prefix xsd: <http://www.w3.org/2001/XMLSchema#> + prefix bsfs: <http://bsfs.ai/schema/> + + 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: <http://www.w3.org/2000/01/rdf-schema#> + prefix xsd: <http://www.w3.org/2001/XMLSchema#> + prefix bsfs: <http://bsfs.ai/schema/> + prefix bse: <http://bsfs.ai/schema/Entity#> + + 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: <http://www.w3.org/2000/01/rdf-schema#> + prefix xsd: <http://www.w3.org/2001/XMLSchema#> + prefix bsfs: <http://bsfs.ai/schema/> + + 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: <http://www.w3.org/2000/01/rdf-schema#> + prefix xsd: <http://www.w3.org/2001/XMLSchema#> + prefix bsfs: <http://bsfs.ai/schema/> + prefix bse: <http://bsfs.ai/schema/Entity#> + + 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: <http://www.w3.org/2000/01/rdf-schema#> + prefix xsd: <http://www.w3.org/2001/XMLSchema#> + prefix bsfs: <http://bsfs.ai/schema/> + prefix bse: <http://bsfs.ai/schema/Entity#> + + # 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: <http://www.w3.org/2000/01/rdf-schema#> + prefix xsd: <http://www.w3.org/2001/XMLSchema#> + prefix bsfs: <http://bsfs.ai/schema/> + prefix bse: <http://bsfs.ai/schema/Entity#> + + 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: <http://www.w3.org/2000/01/rdf-schema#> + prefix xsd: <http://www.w3.org/2001/XMLSchema#> + prefix bsfs: <http://bsfs.ai/schema/> + + xsd:string rdfs:subClassOf bsfs:Literal . + + ''').literal(ns.xsd.string).annotations, {}) + self.assertDictEqual(from_string(''' + prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> + prefix xsd: <http://www.w3.org/2001/XMLSchema#> + prefix bsfs: <http://bsfs.ai/schema/> + + 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: <http://www.w3.org/2000/01/rdf-schema#> + prefix xsd: <http://www.w3.org/2001/XMLSchema#> + prefix bsfs: <http://bsfs.ai/schema/> + prefix bse: <http://bsfs.ai/schema/Entity#> + + 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: <http://www.w3.org/2000/01/rdf-schema#> + prefix xsd: <http://www.w3.org/2001/XMLSchema#> + prefix bsfs: <http://bsfs.ai/schema/> + prefix bse: <http://bsfs.ai/schema/Entity#> + + 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: <http://www.w3.org/2000/01/rdf-schema#> + prefix xsd: <http://www.w3.org/2001/XMLSchema#> + prefix bsfs: <http://bsfs.ai/schema/> + prefix bse: <http://bsfs.ai/schema/Entity#> + + 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: <http://www.w3.org/2000/01/rdf-schema#> + prefix xsd: <http://www.w3.org/2001/XMLSchema#> + prefix bsfs: <http://bsfs.ai/schema/> + prefix bse: <http://bsfs.ai/schema/Entity#> + + 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: <http://www.w3.org/2000/01/rdf-schema#> + prefix xsd: <http://www.w3.org/2001/XMLSchema#> + prefix bsfs: <http://bsfs.ai/schema/> + prefix bse: <http://bsfs.ai/schema/Entity#> + + 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: <http://www.w3.org/2000/01/rdf-schema#> + prefix xsd: <http://www.w3.org/2001/XMLSchema#> + prefix bsfs: <http://bsfs.ai/schema/> + prefix bse: <http://bsfs.ai/schema/Entity#> + + 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: <http://www.w3.org/2000/01/rdf-schema#> + prefix xsd: <http://www.w3.org/2001/XMLSchema#> + prefix bsfs: <http://bsfs.ai/schema/> + prefix bse: <http://bsfs.ai/schema/Entity#> + + 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: <http://www.w3.org/2000/01/rdf-schema#> + prefix xsd: <http://www.w3.org/2001/XMLSchema#> + prefix bsfs: <http://bsfs.ai/schema/> + prefix bse: <http://bsfs.ai/schema/Entity#> + + 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: <http://www.w3.org/2000/01/rdf-schema#> + prefix xsd: <http://www.w3.org/2001/XMLSchema#> + prefix bsfs: <http://bsfs.ai/schema/> + prefix bse: <http://bsfs.ai/schema/Entity#> + + 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: <http://www.w3.org/2000/01/rdf-schema#> + prefix xsd: <http://www.w3.org/2001/XMLSchema#> + prefix bsfs: <http://bsfs.ai/schema/> + prefix bse: <http://bsfs.ai/schema/Entity#> + + 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: <http://www.w3.org/2000/01/rdf-schema#> + prefix xsd: <http://www.w3.org/2001/XMLSchema#> + prefix bsfs: <http://bsfs.ai/schema/> + prefix bse: <http://bsfs.ai/schema/Entity#> + + 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: <http://www.w3.org/2000/01/rdf-schema#> + prefix xsd: <http://www.w3.org/2001/XMLSchema#> + prefix bsfs: <http://bsfs.ai/schema/> + prefix bse: <http://bsfs.ai/schema/Entity#> + + 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: <http://www.w3.org/2000/01/rdf-schema#> + prefix xsd: <http://www.w3.org/2001/XMLSchema#> + prefix bsfs: <http://bsfs.ai/schema/> + prefix bse: <http://bsfs.ai/schema/Entity#> + + 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: <http://www.w3.org/2000/01/rdf-schema#> + prefix xsd: <http://www.w3.org/2001/XMLSchema#> + prefix bsfs: <http://bsfs.ai/schema/> + prefix bse: <http://bsfs.ai/schema/Entity#> + + 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: <http://www.w3.org/2000/01/rdf-schema#> + prefix xsd: <http://www.w3.org/2001/XMLSchema#> + prefix bsfs: <http://bsfs.ai/schema/> + prefix bse: <http://bsfs.ai/schema/Entity#> + + 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: <http://www.w3.org/2000/01/rdf-schema#> + prefix xsd: <http://www.w3.org/2001/XMLSchema#> + prefix bsfs: <http://bsfs.ai/schema/> + prefix bse: <http://bsfs.ai/schema/Entity#> + + 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: <http://www.w3.org/2000/01/rdf-schema#> + prefix xsd: <http://www.w3.org/2001/XMLSchema#> + prefix bsfs: <http://bsfs.ai/schema/> + prefix bse: <http://bsfs.ai/schema/Entity#> + + 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: <http://www.w3.org/2000/01/rdf-schema#> + prefix xsd: <http://www.w3.org/2001/XMLSchema#> + prefix bsfs: <http://bsfs.ai/schema/> + prefix bse: <http://bsfs.ai/schema/Entity#> + + bse:comment rdfs:subClassOf bsfs:Predicate ; + rdfs:range bsfs:Node . + + ''').predicate(ns.bse.comment).annotations, {}) + self.assertDictEqual(from_string(''' + prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> + prefix xsd: <http://www.w3.org/2001/XMLSchema#> + prefix bsfs: <http://bsfs.ai/schema/> + prefix bse: <http://bsfs.ai/schema/Entity#> + + 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: <http://www.w3.org/2000/01/rdf-schema#> + prefix xsd: <http://www.w3.org/2001/XMLSchema#> + prefix bsfs: <http://bsfs.ai/schema/> + prefix bse: <http://bsfs.ai/schema/Entity#> + + 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: <http://www.w3.org/2000/01/rdf-schema#> + prefix xsd: <http://www.w3.org/2001/XMLSchema#> + prefix bsfs: <http://bsfs.ai/schema/> + prefix bse: <http://bsfs.ai/schema/Entity#> + + 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: <http://www.w3.org/2000/01/rdf-schema#> + prefix xsd: <http://www.w3.org/2001/XMLSchema#> + prefix bsfs: <http://bsfs.ai/schema/> + prefix bse: <http://bsfs.ai/schema/Entity#> + + 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: <http://www.w3.org/2000/01/rdf-schema#> + prefix xsd: <http://www.w3.org/2001/XMLSchema#> + prefix bsfs: <http://bsfs.ai/schema/> + prefix bse: <http://bsfs.ai/schema/Entity#> + + 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: <http://www.w3.org/2000/01/rdf-schema#> + prefix xsd: <http://www.w3.org/2001/XMLSchema#> + prefix bsfs: <http://bsfs.ai/schema/> + prefix bse: <http://bsfs.ai/schema/Entity#> + + 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: <http://www.w3.org/2000/01/rdf-schema#> + prefix xsd: <http://www.w3.org/2001/XMLSchema#> + prefix bsfs: <http://bsfs.ai/schema/> + prefix bse: <http://bsfs.ai/schema/Entity#> + + 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: <http://www.w3.org/2000/01/rdf-schema#> + prefix xsd: <http://www.w3.org/2001/XMLSchema#> + prefix bsfs: <http://bsfs.ai/schema/> + prefix bse: <http://bsfs.ai/schema/Entity#> + + 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: <http://www.w3.org/2000/01/rdf-schema#> + prefix xsd: <http://www.w3.org/2001/XMLSchema#> + prefix bsfs: <http://bsfs.ai/schema/> + prefix bse: <http://bsfs.ai/schema/Entity#> + + 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: <http://www.w3.org/2000/01/rdf-schema#> + prefix xsd: <http://www.w3.org/2001/XMLSchema#> + prefix bsfs: <http://bsfs.ai/schema/> + prefix bse: <http://bsfs.ai/schema/Entity#> + + 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: <http://www.w3.org/2000/01/rdf-schema#> + prefix xsd: <http://www.w3.org/2001/XMLSchema#> + prefix bsfs: <http://bsfs.ai/schema/> + prefix bse: <http://bsfs.ai/schema/Entity#> + + 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: <http://www.w3.org/2000/01/rdf-schema#> + prefix xsd: <http://www.w3.org/2001/XMLSchema#> + prefix bsfs: <http://bsfs.ai/schema/> + prefix bse: <http://bsfs.ai/schema/Entity#> + + 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: <http://www.w3.org/2000/01/rdf-schema#> + prefix xsd: <http://www.w3.org/2001/XMLSchema#> + prefix bsfs: <http://bsfs.ai/schema/> + prefix bse: <http://bsfs.ai/schema/Entity#> + + 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: <http://www.w3.org/2000/01/rdf-schema#> + prefix xsd: <http://www.w3.org/2001/XMLSchema#> + prefix bsfs: <http://bsfs.ai/schema/> + prefix bse: <http://bsfs.ai/schema/Entity#> + + 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: <http://www.w3.org/2000/01/rdf-schema#> + prefix xsd: <http://www.w3.org/2001/XMLSchema#> + prefix bsfs: <http://bsfs.ai/schema/> + prefix bse: <http://bsfs.ai/schema/Entity#> + + 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: <http://www.w3.org/2000/01/rdf-schema#> + prefix xsd: <http://www.w3.org/2001/XMLSchema#> + prefix bsfs: <http://bsfs.ai/schema/> + prefix bse: <http://bsfs.ai/schema/Entity#> + + 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: <http://www.w3.org/2000/01/rdf-schema#> + prefix xsd: <http://www.w3.org/2001/XMLSchema#> + prefix bsfs: <http://bsfs.ai/schema/> + prefix bse: <http://bsfs.ai/schema/Entity#> + + 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: <http://www.w3.org/2000/01/rdf-schema#> + prefix xsd: <http://www.w3.org/2001/XMLSchema#> + prefix bsfs: <http://bsfs.ai/schema/> + prefix bse: <http://bsfs.ai/schema/Entity#> + + 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: <http://www.w3.org/2000/01/rdf-schema#> + prefix xsd: <http://www.w3.org/2001/XMLSchema#> + + # bsfs prefixes + prefix bsfs: <http://bsfs.ai/schema/> + prefix bse: <http://bsfs.ai/schema/Entity#> + + # 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 + <http://bsfs.ai/schema/Feature/colors_spatial> 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 . + + <http://bsfs.ai/schema/Feature/colors_spatial#1234> rdfs:subClassOf <http://bsfs.ai/schema/Feature/colors_spatial> ; + bsfs:dimension "1024"^^xsd:integer ; + rdfs:label "Main colors spatial instance"^^xsd:string . + + <http://bsfs.ai/schema/Feature/colors_spatial#4321> rdfs:subClassOf <http://bsfs.ai/schema/Feature/colors_spatial> ; + 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 ## - |