diff options
author | Matthias Baumgartner <dev@igsor.net> | 2022-12-08 16:34:13 +0100 |
---|---|---|
committer | Matthias Baumgartner <dev@igsor.net> | 2022-12-08 16:34:13 +0100 |
commit | 7eb61d117a995b076d36c55d2c7c268665360813 (patch) | |
tree | c47a0fe523349dd3a14c1501281d42347c3b8954 | |
parent | 729f025f392d45b621941da9d052834e0d81506e (diff) | |
download | bsfs-7eb61d117a995b076d36c55d2c7c268665360813.tar.gz bsfs-7eb61d117a995b076d36c55d2c7c268665360813.tar.bz2 bsfs-7eb61d117a995b076d36c55d2c7c268665360813.zip |
schema
-rw-r--r-- | bsfs/schema/__init__.py | 24 | ||||
-rw-r--r-- | bsfs/schema/schema.py | 325 | ||||
-rw-r--r-- | bsfs/schema/types.py | 269 | ||||
-rw-r--r-- | test/schema/__init__.py | 0 | ||||
-rw-r--r-- | test/schema/test_schema.py | 616 | ||||
-rw-r--r-- | test/schema/test_types.py | 225 |
6 files changed, 1459 insertions, 0 deletions
diff --git a/bsfs/schema/__init__.py b/bsfs/schema/__init__.py new file mode 100644 index 0000000..ce381ec --- /dev/null +++ b/bsfs/schema/__init__.py @@ -0,0 +1,24 @@ +""" + +Part of the BlackStar filesystem (bsfs) module. +A copy of the license is provided with the project. +Author: Matthias Baumgartner, 2022 +""" +# imports +import typing + +# inner-module imports +#from . import types +from .schema import Schema +from .types import Literal, Node, Predicate + +# exports +__all__: typing.Sequence[str] = ( + 'Literal', + 'Node', + 'Predicate', + 'Schema', + #'types', + ) + +## EOF ## diff --git a/bsfs/schema/schema.py b/bsfs/schema/schema.py new file mode 100644 index 0000000..0e053c0 --- /dev/null +++ b/bsfs/schema/schema.py @@ -0,0 +1,325 @@ +""" + +Part of the BlackStar filesystem (bsfs) module. +A copy of the license is provided with the project. +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 +from . import types + +# exports +__all__: typing.Sequence[str] = ( + 'Schema', + ) + + +## code ## + +class Schema(): + """ + """ + + _nodes: typing.Dict[URI, types.Node] + _literals: typing.Dict[URI, types.Literal] + _predicates: typing.Dict[URI, types.Predicate] + + def __init__( + self, + predicates: typing.Iterable[types.Predicate], + nodes: typing.Optional[typing.Iterable[types.Node]] = None, + literals: typing.Optional[typing.Iterable[types.Literal]] = None, + ): + # materialize arguments + if nodes is None: + nodes = set() + if literals is None: + literals = set() + nodes = set(nodes) + literals = set(literals) + predicates = set(predicates) + # include parents in predicates set + predicates |= {par for pred in predicates for par in pred.parents()} + # 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} + nodes |= {vert for vert in prange if isinstance(vert, types.Node)} + literals |= {vert for vert in prange if isinstance(vert, types.Literal)} + # 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. + nodes |= {par for node in nodes for par in node.parents()} + literals |= {par for lit in literals for par in lit.parents()} + # 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') + if len(literals) != len(self._literals): + raise errors.ConsistencyError('inconsistent literals') + if len(predicates) != len(self._predicates): + raise errors.ConsistencyError('inconsistent predicates') + # verify globally unique uris + n_uris = len(set(self._nodes) | set(self._literals) | set(self._predicates)) + if n_uris != len(self._nodes) + len(self._literals) + len(self._predicates): + raise errors.ConsistencyError('URI dual use') + + + ## essentials ## + + def __str__(self) -> str: + return f'{typename(self)}()' + + def __repr__(self) -> str: + return f'{typename(self)}({sorted(self._nodes)}, {sorted(self._literals)}, {sorted(self._predicates)})' + + def __hash__(self) -> int: + return hash(( + type(self), + tuple(sorted(self._nodes.values())), + tuple(sorted(self._literals.values())), + tuple(sorted(self._predicates.values())), + )) + + def __eq__(self, other: typing.Any) -> bool: + return isinstance(other, type(self)) \ + and self._nodes == other._nodes \ + and self._literals == other._literals \ + and self._predicates == other._predicates + + + ## operators ## + + SchemaDiff = namedtuple('SchemaDiff', ['nodes', 'literals', 'predicates']) + + def diff(self, other: 'Schema') -> SchemaDiff: + """Return node, literals, and predicates that are in *self* but not in *other*.""" + return self.SchemaDiff( + nodes=set(self.nodes()) - set(other.nodes()), + literals=set(self.literals()) - set(other.literals()), + predicates=set(self.predicates()) - set(other.predicates()), + ) + + def __sub__(self, other: typing.Any) -> SchemaDiff: + """Alias for `Schema.diff`.""" + if not isinstance(other, Schema): + return NotImplemented + return self.diff(other) + + def consistent_with(self, other: 'Schema') -> bool: + """Checks if two schemas have different definitions for the same uri. + Tests nodes, literals, and predicates. + """ + # check arg + if not isinstance(other, Schema): + raise TypeError(other) + # node consistency + nodes = set(self.nodes()) | set(other.nodes()) + nuris = {node.uri for node in nodes} + if len(nodes) != len(nuris): + return False + # literal consistency + literals = set(self.literals()) | set(other.literals()) + luris = {lit.uri for lit in literals} + if len(literals) != len(luris): + return False + # predicate consistency + predicates = set(self.predicates()) | set(other.predicates()) + puris = {pred.uri for pred in predicates} + if len(predicates) != len(puris): + return False + # global consistency + if len(puris | luris | nuris) != len(nodes) + len(literals) + len(predicates): + return False + # all checks passed + return True + + @classmethod + def Union(cls, *args: typing.Union['Schema', typing.Iterable['Schema']]) -> 'Schema': + """Combine multiple Schema instances into a single one. + As argument, you can either pass multiple Schema instances, or a single + iterable over Schema instances. Any abc.Iterable will be accepted. + + Example: + + >>> a, b, c = Schema.Empty(), Schema.Empty(), Schema.Empty() + >>> # multiple Schema instances + >>> Schema.Union(a, b, c) + >>> # A single iterable over Schema instances + >>> Schema.Union([a, b, c]) + + """ + 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 + pass + elif len(args) == 1 and isinstance(args[0], abc.Iterable): # args is a single iterable + args = args[0] + else: + raise TypeError(f'expected multiple Schema instances or a single Iterable, found {args}') + + nodes, literals, predicates = set(), set(), set() + for schema in args: + # check argument + if not isinstance(schema, cls): + raise TypeError(schema) + # merge with previous schemas + nodes |= set(schema.nodes()) + literals |= set(schema.literals()) + predicates |= set(schema.predicates()) + # return new Schema instance + return cls(predicates, nodes, literals) + + def union(self, other: 'Schema') -> 'Schema': + """Merge *other* and *self* into a new Schema. *self* takes precedence.""" + # check type + if not isinstance(other, type(self)): + raise TypeError(other) + # return combined schemas + return self.Union(self, other) + + def __add__(self, other: typing.Any) -> 'Schema': + """Alias for Schema.union.""" + try: # return merged schemas + return self.union(other) + except TypeError: + return NotImplemented + + def __or__(self, other: typing.Any) -> 'Schema': + """Alias for Schema.union.""" + return self.__add__(other) + + + ## getters ## + # FIXME: which of the getters below are actually needed? + # FIXME: interchangeability of URI and _Type?! + + def has_node(self, node: URI) -> bool: + return node in self._nodes + + def has_literal(self, lit: URI) -> bool: + return lit in self._literals + + def has_predicate(self, pred: URI) -> bool: + return pred in self._predicates + + def nodes(self) -> typing.Iterator[types.Node]: # FIXME: type annotation + return self._nodes.values() + + def literals(self) -> typing.Iterator[types.Literal]: # FIXME: type annotation + return self._literals.values() + + def predicates(self) -> typing.Iterator[types.Predicate]: # FIXME: type annotation + return self._predicates.values() + + def node(self, uri: URI) -> types.Node: + """Return the Node matching the *uri*.""" + return self._nodes[uri] + + def predicate(self, uri: URI) -> types.Predicate: + """Return the Predicate matching the *uri*.""" + return self._predicates[uri] + + def literal(self, uri: URI) -> types.Literal: + """Return the Literal matching the *uri*.""" + return self._literals[uri] + + + ## constructors ## + + + @classmethod + def Empty(cls) -> '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': + """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/types.py b/bsfs/schema/types.py new file mode 100644 index 0000000..6e257e3 --- /dev/null +++ b/bsfs/schema/types.py @@ -0,0 +1,269 @@ +""" + +Part of the BlackStar filesystem (bsfs) module. +A copy of the license is provided with the project. +Author: Matthias Baumgartner, 2022 +""" +# imports +import typing + +# bsfs imports +from bsfs.utils import errors, URI, typename + +# exports +__all__: typing.Sequence[str] = ( + 'Literal', + 'Node', + 'Predicate', + ) + + +## code ## + +class _Type(): + """A class is defined via its uri. + + Classes define a partial order. + The order operators indicate whether some class is a + superclass (greater-than) or a subclass (less-than) of another. + Comparisons are only supported within the same type. + + For example, consider the class hierarchy below: + + Vehicle + Two-wheel + Bike + Bicycle + + >>> vehicle = _Type('Vehicle') + >>> twowheel = _Type('Two-wheel', vehicle) + >>> bike = _Type('Bike', twowheel) + >>> bicycle = _Type('Bicycle', twowheel) + + Two-wheel is equivalent to itself + >>> twowheel == vehicle + False + >>> twowheel == twowheel + True + >>> twowheel == bicycle + False + + Two-wheel is a true subclass of Vehicle + >>> twowheel < vehicle + True + >>> twowheel < twowheel + False + >>> twowheel < bicycle + False + + Two-wheel is a subclass of itself and Vehicle + >>> twowheel <= vehicle + True + >>> twowheel <= twowheel + True + >>> twowheel <= bicycle + False + + Two-wheel is a true superclass of Bicycle + >>> twowheel > vehicle + False + >>> twowheel > twowheel + False + >>> twowheel > bicycle + True + + Two-wheel is a superclass of itself and Bicycle + >>> twowheel >= vehicle + False + >>> twowheel >= twowheel + True + >>> twowheel >= bicycle + True + + Analoguous to sets, this is not a total order: + >>> bike < bicycle + False + >>> bike > bicycle + False + >>> bike == bicycle + False + """ + + # class uri. + uri: URI + + # parent's class uris. + parent: typing.Optional['_Type'] + + def __init__( + self, + uri: URI, + parent: typing.Optional['_Type'] = None, + ): + self.uri = uri + self.parent = parent + + def parents(self) -> typing.Generator['_Type', None, None]: + """Generate a list of parent nodes.""" + curr = self.parent + while curr is not None: + yield curr + curr = curr.parent + + def get_child(self, uri: URI, **kwargs): + """Return a child of the current class.""" + return type(self)(uri, self, **kwargs) + + def __str__(self) -> str: + return f'{typename(self)}({self.uri})' + + def __repr__(self) -> str: + return f'{typename(self)}({self.uri}, {repr(self.parent)})' + + def __hash__(self) -> int: + return hash((type(self), self.uri, self.parent)) + + def __eq__(self, other: typing.Any) -> bool: + """Return True iff *self* is equivalent to *other*.""" + return type(self) == type(other) \ + and self.uri == other.uri \ + and self.parent == other.parent + + def __lt__(self, other: typing.Any) -> bool: + """Return True iff *self* is a true subclass of *other*.""" + if not type(self) == type(other): # type mismatch + return NotImplemented + elif self.uri == other.uri: # equivalence + return False + elif self in other.parents(): # superclass + return False + elif other in self.parents(): # subclass + return True + else: # not related + return False + + def __le__(self, other: typing.Any) -> bool: + """Return True iff *self* is equivalent or a subclass of *other*.""" + if not type(self) == type(other): # type mismatch + return NotImplemented + elif self.uri == other.uri: # equivalence + return True + elif self in other.parents(): # superclass + return False + elif other in self.parents(): # subclass + return True + else: # not related + return False + + def __gt__(self, other: typing.Any) -> bool: + """Return True iff *self* is a true superclass of *other*.""" + if not type(self) == type(other): # type mismatch + return NotImplemented + elif self.uri == other.uri: # equivalence + return False + elif self in other.parents(): # superclass + return True + elif other in self.parents(): # subclass + return False + else: # not related + return False + + def __ge__(self, other: typing.Any) -> bool: + """Return True iff *self* is eqiuvalent or a superclass of *other*.""" + if not type(self) == type(other): # type mismatch + return NotImplemented + elif self.uri == other.uri: # equivalence + return True + elif self in other.parents(): # superclass + return True + elif other in self.parents(): # subclass + return False + else: # not related + return False + + +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) + + +class Node(_Vertex): + """Node type.""" + def __init__(self, uri: URI, parent: typing.Optional['Node']): + super().__init__(uri, parent) + + +class Literal(_Vertex): + """Literal type.""" + def __init__(self, uri: URI, parent: typing.Optional['Literal']): + super().__init__(uri, parent) + + +class Predicate(_Type): + """Predicate type.""" + + # source type. + domain: Node + + # destination type. + range: typing.Optional[typing.Union[Node, Literal]] + + # maximum cardinality of type. + unique: bool + + def __init__( + self, + # Type members + uri: URI, + parent: 'Predicate', + # Predicate members + domain: Node, + range: typing.Optional[typing.Union[Node, Literal]], + unique: bool, + ): + # 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): + raise TypeError(range) + # initialize + super().__init__(uri, parent) + self.domain = domain + self.range = range + self.unique = unique + + def __hash__(self) -> int: + return hash((super().__hash__(), self.domain, self.range, self.unique)) + + def __eq__(self, other: typing.Any) -> bool: + return super().__eq__(other) \ + and self.domain == other.domain \ + and self.range == other.range \ + and self.unique == other.unique + + def get_child( + self, + uri: URI, + domain: typing.Optional[Node] = None, + range: typing.Optional[_Vertex] = None, + unique: typing.Optional[bool] = None, + **kwargs, + ): + """Return a child of the current class.""" + if domain is None: + domain = self.domain + if not domain <= self.domain: + 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: + 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) + + +## EOF ## diff --git a/test/schema/__init__.py b/test/schema/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/test/schema/__init__.py diff --git a/test/schema/test_schema.py b/test/schema/test_schema.py new file mode 100644 index 0000000..2dc26e8 --- /dev/null +++ b/test/schema/test_schema.py @@ -0,0 +1,616 @@ +""" + +Part of the tagit test suite. +A copy of the license is provided with the project. +Author: Matthias Baumgartner, 2022 +""" +# imports +import operator +import unittest + +# bsfs imports +from bsfs.namespace import ns +from bsfs.schema import types +from bsfs.utils import errors + +# objects to test +from bsfs.schema.schema import Schema + + +## code ## + +class TestSchema(unittest.TestCase): + + def setUp(self): + self.schema_str = ''' + 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:Tag rdfs:subClassOf bsfs:Node . + bsfs:Image rdfs:subClassOf bsfs:Entity . + bsfs:Unused rdfs:subClassOf bsfs:Node . + + xsd:string rdfs:subClassOf bsfs:Literal . + xsd:integer rdfs:subClassOf bsfs:Literal . + xsd:boolean rdfs:subClassOf bsfs:Literal . + + bse:tag rdfs:subClassOf bsfs:Predicate ; + rdfs:domain bsfs:Entity ; + rdfs:range bsfs:Tag ; + bsfs:unique "false"^^xsd:boolean . + + bse:group rdfs:subClassOf bse:tag ; + rdfs:domain bsfs:Image ; + rdfs:range bsfs:Tag ; + bsfs:unique "false"^^xsd:boolean . + + bse:comment rdfs:subClassOf bsfs:Predicate ; + rdfs:domain bsfs:Node ; + rdfs:range xsd:string ; + bsfs:unique "true"^^xsd:boolean . + + ''' + # nodes + self.n_root = types.Node(ns.bsfs.Node, None) + 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.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_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_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] + + # predicates + self.p_root = types.Predicate(ns.bsfs.Predicate, None, types.Node(ns.bsfs.Node, None), None, False) + 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] + + def test_construction(self): + # 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.predicates()), set(self.predicates)) + + # predicates, nodes, and literals are respected + schema = Schema(self.predicates, self.nodes, self.literals) + self.assertSetEqual(set(schema.nodes()), set(self.nodes)) + self.assertSetEqual(set(schema.literals()), set(self.literals)) + self.assertSetEqual(set(schema.predicates()), set(self.predicates)) + + # nodes are complete (w/o unused) + schema = Schema(self.predicates, None, self.literals) + self.assertSetEqual(set(schema.nodes()), {self.n_root, self.n_ent, self.n_img, self.n_tag}) + schema = Schema(self.predicates, [], self.literals) + self.assertSetEqual(set(schema.nodes()), {self.n_root, self.n_ent, self.n_img, self.n_tag}) + schema = Schema(self.predicates, [self.n_img, self.n_tag], self.literals) + self.assertSetEqual(set(schema.nodes()), {self.n_root, self.n_ent, self.n_img, self.n_tag}) + schema = Schema(self.predicates, [self.n_unused], self.literals) + self.assertSetEqual(set(schema.nodes()), set(self.nodes)) + + # literals are complete + schema = Schema(self.predicates, self.nodes, None) + self.assertSetEqual(set(schema.literals()), {self.l_root, self.l_string}) + schema = Schema(self.predicates, self.nodes, []) + self.assertSetEqual(set(schema.literals()), {self.l_root, self.l_string}) + schema = Schema(self.predicates, self.nodes, [self.l_string]) + self.assertSetEqual(set(schema.literals()), {self.l_root, self.l_string}) + schema = Schema(self.predicates, self.nodes, [self.l_integer]) + self.assertSetEqual(set(schema.literals()), {self.l_root, self.l_string, self.l_integer}) + 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()) + schema = Schema([self.p_group], self.nodes, self.literals) + 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)) + + # node uris must be unique + self.assertRaises(errors.ConsistencyError, Schema, self.predicates, + self.nodes + [types.Node(ns.bsfs.Entity, None)], self.literals) + self.assertRaises(errors.ConsistencyError, Schema, self.predicates, + self.nodes + [types.Node(ns.bsfs.Entity, types.Node(ns.bsfs.Foo, None))], self.literals) + self.assertRaises(errors.ConsistencyError, Schema, self.predicates, + self.nodes + [types.Node(ns.bsfs.Entity, self.n_img)], self.literals) + self.assertRaises(errors.ConsistencyError, Schema, self.predicates, + [types.Node(ns.bsfs.Entity, self.n_img)], self.literals) + + # literal uris must be unique + self.assertRaises(errors.ConsistencyError, Schema, self.predicates, self.nodes, + self.literals + [types.Literal(ns.xsd.string, None)]) + self.assertRaises(errors.ConsistencyError, Schema, self.predicates, self.nodes, + self.literals + [types.Literal(ns.xsd.string, types.Literal(ns.bsfs.Foo, None))]) + self.assertRaises(errors.ConsistencyError, Schema, self.predicates, self.nodes, + self.literals + [types.Literal(ns.xsd.string, self.l_integer)]) + self.assertRaises(errors.ConsistencyError, Schema, self.predicates, self.nodes, + [types.Literal(ns.xsd.string, self.l_integer)]) + + # predicate uris must be unique + self.assertRaises(errors.ConsistencyError, Schema, + self.predicates + [types.Predicate(ns.bse.tag, self.p_root, self.n_root, self.n_tag, False)]) + self.assertRaises(errors.ConsistencyError, Schema, + self.predicates + [types.Predicate(ns.bse.tag, self.p_root, self.n_ent, self.n_img, False)]) + self.assertRaises(errors.ConsistencyError, Schema, + self.predicates + [types.Predicate(ns.bse.tag, self.p_root, self.n_ent, self.n_tag, True)]) + self.assertRaises(errors.ConsistencyError, Schema, + self.predicates + [types.Predicate(ns.bse.tag, None, self.n_ent, self.n_tag, False)]) + + # uris must be unique across nodes, literals, and predicates + 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)}) + 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)}, {}) + 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)}) + + def test_str(self): + 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([], [], [])') + 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] + self.assertEqual(repr(Schema(self.predicates, self.nodes, self.literals)), f'Schema({n}, {l}, {p})') + + def test_equality(self): + schema = Schema(self.predicates, self.nodes, self.literals) + # instance is equal to itself + self.assertEqual(schema, schema) + self.assertEqual(hash(schema), hash(schema)) + # instance is equal to a clone + self.assertEqual(schema, Schema(self.predicates, self.nodes, self.literals)) + self.assertEqual(hash(schema), hash(Schema(self.predicates, self.nodes, self.literals))) + # equality respects nodes + self.assertNotEqual(schema, + Schema(self.predicates, [self.n_root, self.n_ent, self.n_img, self.n_tag], self.literals)) + self.assertNotEqual(hash(schema), + hash(Schema(self.predicates, [self.n_root, self.n_ent, self.n_img, self.n_tag], self.literals))) + self.assertNotEqual(schema, + Schema(self.predicates, self.nodes + [types.Node(ns.bsfs.Document, self.n_ent)], self.literals)) + self.assertNotEqual(hash(schema), + hash(Schema(self.predicates, self.nodes + [types.Node(ns.bsfs.Document, self.n_ent)], self.literals))) + # equality respects literals + self.assertNotEqual(schema, + Schema(self.predicates, self.nodes, [self.l_root, self.l_string, self.l_integer])) + self.assertNotEqual(hash(schema), + hash(Schema(self.predicates, self.nodes, [self.l_root, self.l_string, self.l_integer]))) + self.assertNotEqual(schema, + Schema(self.predicates, self.nodes, self.literals + [types.Literal(ns.xsd.number, self.l_root)])) + self.assertNotEqual(hash(schema), + hash(Schema(self.predicates, self.nodes, self.literals + [types.Literal(ns.xsd.number, self.l_root)]))) + # equality respects predicates + self.assertNotEqual(schema, + Schema([self.p_group, self.p_tag, self.p_root], self.nodes, self.literals)) + 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)) + 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))) + + def test_diff(self): + # difference can be empty + diff = Schema({self.p_tag}).diff(Schema({self.p_group})) + self.assertSetEqual(set(diff.nodes), set()) + self.assertSetEqual(set(diff.literals), set()) + self.assertSetEqual(set(diff.predicates), set()) + + # difference contains predicates from the LHS + diff = Schema({self.p_group}).diff(Schema({self.p_tag})) + self.assertSetEqual(set(diff.nodes), {self.n_img}) + self.assertSetEqual(set(diff.literals), set()) + self.assertSetEqual(set(diff.predicates), {self.p_group}) + + # 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.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.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.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.predicates), {self.p_comment}) + # __sub__ only accepts Schema instances + class Foo(): pass + self.assertRaises(TypeError, operator.sub, Schema({self.p_comment}, {self.n_unused}, {self.l_unused}), 1234) + self.assertRaises(TypeError, operator.sub, Schema({self.p_comment}, {self.n_unused}, {self.l_unused}), 'hello world') + self.assertRaises(TypeError, operator.sub, Schema({self.p_comment}, {self.n_unused}, {self.l_unused}), Foo()) + + def test_consistent_with(self): + # argument must be a schema + class Foo(): pass + self.assertRaises(TypeError, Schema([]).consistent_with, 1234) + self.assertRaises(TypeError, Schema([]).consistent_with, 'hello world') + self.assertRaises(TypeError, Schema([]).consistent_with, Foo()) + + # node consistency + self.assertTrue(Schema([], {self.n_ent, self.n_tag, self.n_unused}).consistent_with( + Schema(self.predicates))) + self.assertFalse(Schema([], {types.Node(ns.bsfs.Entity, None)}).consistent_with( + Schema(self.predicates))) + # order doesn't matter + self.assertTrue(Schema(self.predicates).consistent_with( + Schema([], {self.n_ent, self.n_tag, self.n_unused}))) + + # literal consistency + self.assertTrue(Schema([], [], {self.l_string, self.l_unused}).consistent_with( + Schema(self.predicates))) + self.assertFalse(Schema([], [], {types.Literal(ns.xsd.string, None)}).consistent_with( + Schema(self.predicates))) + # order doesn't matter + self.assertTrue(Schema(self.predicates).consistent_with( + Schema([], [], {self.l_string, self.l_unused}))) + + # predicate consistency + self.assertTrue(Schema({self.p_tag}).consistent_with( + Schema(self.predicates))) + self.assertFalse(Schema({types.Predicate(ns.bse.tag, None, self.n_root, self.n_root, False)}).consistent_with( + Schema(self.predicates))) + # order doesn't matter + self.assertTrue(Schema(self.predicates).consistent_with( + Schema({self.p_tag}))) + + # global consistency + self.assertFalse(Schema({types.Predicate(ns.bsfs.Entity, None, self.n_root, self.n_root, False)}).consistent_with( + Schema(self.predicates))) + self.assertFalse(Schema([], {types.Node(ns.xsd.string, None)}).consistent_with( + Schema(self.predicates))) + self.assertFalse(Schema([], [], {types.Literal(ns.bsfs.Entity, None)}).consistent_with( + Schema(self.predicates))) + + + def test_union(self): + # must provide at least one schema + self.assertRaises(TypeError, Schema.Union) + + # can pass schemas as list + self.assertEqual(Schema.Union([Schema({self.p_tag})]), Schema({self.p_tag})) + self.assertEqual(Schema.Union([Schema({self.p_tag}), Schema({self.p_comment})]), + Schema({self.p_tag, self.p_comment})) + + # can pass schemas as arguments + self.assertEqual(Schema.Union(Schema({self.p_tag})), Schema({self.p_tag})) + self.assertEqual(Schema.Union(Schema({self.p_tag}), Schema({self.p_comment})), + Schema({self.p_tag, self.p_comment})) + + # cannot mix the two argument passing styles + self.assertRaises(TypeError, Schema.Union, [Schema(self.predicates)], Schema(self.predicates)) + + # all arguments must be Schema instances + self.assertRaises(TypeError, Schema.Union, Schema(self.predicates), 1234) + self.assertRaises(TypeError, Schema.Union, Schema(self.predicates), 1234, Schema(self.predicates)) + self.assertRaises(TypeError, Schema.Union, Schema(self.predicates), 'hello world') + + # Union merges predicates, nodes, and literals + self.assertEqual(Schema.Union( + Schema({self.p_comment}, {self.n_unused}, {}), + Schema({self.p_group}, {self.n_img}, {self.l_unused})), + Schema({self.p_comment, self.p_group}, {self.n_img, self.n_unused}, {self.l_unused})) + + # Union does not accept inconsistent nodes + self.assertRaises(errors.ConsistencyError, Schema.Union, Schema(self.predicates), + Schema({}, {types.Node(ns.bsfs.Entity, None)})) + self.assertRaises(errors.ConsistencyError, Schema.Union, Schema({}, {self.n_ent}), + Schema({}, {types.Node(ns.bsfs.Entity, None)})) + self.assertRaises(errors.ConsistencyError, Schema.Union, Schema({}, {self.n_ent}), + Schema({}, {}, {types.Literal(ns.bsfs.Entity, None)})) + + # Union does not accept inconsistent literals + self.assertRaises(errors.ConsistencyError, Schema.Union, Schema(self.predicates), + Schema({}, {}, {types.Literal(ns.xsd.string, None)})) + self.assertRaises(errors.ConsistencyError, Schema.Union, Schema({}, {}, {self.l_string}), + Schema({}, {}, {types.Literal(ns.xsd.string, None)})) + self.assertRaises(errors.ConsistencyError, Schema.Union, Schema({}, {}, {self.l_string}), + Schema({}, {types.Node(ns.xsd.string, None)})) + + # Union does not accept inconsistent predicates + self.assertRaises(errors.ConsistencyError, Schema.Union, Schema({self.p_tag}), + Schema({types.Predicate(ns.bse.tag, None, self.n_ent, self.n_tag, False)})) + self.assertRaises(errors.ConsistencyError, Schema.Union, Schema({self.p_tag}), + Schema({}, {types.Node(ns.bse.tag, None)})) + + # union is an alias for Union + self.assertEqual(Schema({self.p_comment}, {self.n_unused}, {}).union( + Schema({self.p_group}, {self.n_img}, {self.l_unused})), + Schema({self.p_comment, self.p_group}, {self.n_img, self.n_unused}, {self.l_unused})) + # union only accepts Schema instances + class Foo(): pass + self.assertRaises(TypeError, Schema({self.p_comment}, {self.n_unused}, {}).union, 1234) + self.assertRaises(TypeError, Schema({self.p_comment}, {self.n_unused}, {}).union, 'hello world') + self.assertRaises(TypeError, Schema({self.p_comment}, {self.n_unused}, {}).union, Foo()) + + # __add__ is an alias for Union + self.assertEqual(Schema({self.p_comment}, {self.n_unused}, {}) + Schema({self.p_group}, {self.n_img}, {self.l_unused}), + Schema({self.p_comment, self.p_group}, {self.n_img, self.n_unused}, {self.l_unused})) + # __add__ only accepts Schema instances + class Foo(): pass + self.assertRaises(TypeError, operator.add, Schema({self.p_comment}, {self.n_unused}, {}), 1234) + self.assertRaises(TypeError, operator.add, Schema({self.p_comment}, {self.n_unused}, {}), 'hello world') + self.assertRaises(TypeError, operator.add, Schema({self.p_comment}, {self.n_unused}, {}), Foo()) + + # __or__ is an alias for Union + self.assertEqual(Schema({self.p_comment}, {self.n_unused}, {}) | Schema({self.p_group}, {self.n_img}, {self.l_unused}), + Schema({self.p_comment, self.p_group}, {self.n_img, self.n_unused}, {self.l_unused})) + # __or__ only accepts Schema instances + class Foo(): pass + self.assertRaises(TypeError, operator.or_, Schema({self.p_comment}, {self.n_unused}, {}), 1234) + self.assertRaises(TypeError, operator.or_, Schema({self.p_comment}, {self.n_unused}, {}), 'hello world') + self.assertRaises(TypeError, operator.or_, Schema({self.p_comment}, {self.n_unused}, {}), Foo()) + + def test_type_getters(self): + schema = Schema(self.predicates, self.nodes, self.literals) + # nodes + self.assertEqual(self.n_root, schema.node(ns.bsfs.Node)) + self.assertEqual(self.n_ent, schema.node(ns.bsfs.Entity)) + self.assertEqual(self.n_img, schema.node(ns.bsfs.Image)) + self.assertRaises(KeyError, schema.node, ns.bsfs.Document) + self.assertRaises(KeyError, schema.node, self.n_root) + # literals + self.assertEqual(self.l_root, schema.literal(ns.bsfs.Literal)) + self.assertEqual(self.l_string, schema.literal(ns.xsd.string)) + self.assertEqual(self.l_integer, schema.literal(ns.xsd.integer)) + self.assertRaises(KeyError, schema.literal, ns.xsd.number) + self.assertRaises(KeyError, schema.literal, self.l_root) + # predicates + self.assertEqual(self.p_root, schema.predicate(ns.bsfs.Predicate)) + self.assertEqual(self.p_tag, schema.predicate(ns.bse.tag)) + self.assertEqual(self.p_group, schema.predicate(ns.bse.group)) + self.assertRaises(KeyError, schema.predicate, ns.bse.mimetype) + self.assertRaises(KeyError, schema.predicate, self.p_root) + + def test_list_getters(self): + schema = Schema(self.predicates, self.nodes, self.literals) + self.assertSetEqual(set(self.nodes), set(schema.nodes())) + self.assertSetEqual(set(self.literals), set(schema.literals())) + self.assertSetEqual(set(self.predicates), set(schema.predicates())) + + def test_has(self): + schema = Schema(self.predicates, self.nodes, self.literals) + # nodes + self.assertTrue(schema.has_node(ns.bsfs.Node)) + self.assertTrue(schema.has_node(ns.bsfs.Entity)) + self.assertTrue(schema.has_node(ns.bsfs.Image)) + self.assertFalse(schema.has_node(ns.bsfs.Document)) + self.assertFalse(schema.has_node(self.n_root)) + # literals + self.assertTrue(schema.has_literal(ns.bsfs.Literal)) + self.assertTrue(schema.has_literal(ns.xsd.string)) + self.assertTrue(schema.has_literal(ns.xsd.integer)) + self.assertFalse(schema.has_literal(ns.xsd.number)) + self.assertFalse(schema.has_literal(self.l_root)) + # predicates + self.assertTrue(schema.has_predicate(ns.bsfs.Predicate)) + self.assertTrue(schema.has_predicate(ns.bse.tag)) + self.assertTrue(schema.has_predicate(ns.bse.group)) + 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__': + unittest.main() + +## EOF ## diff --git a/test/schema/test_types.py b/test/schema/test_types.py new file mode 100644 index 0000000..4a49e6e --- /dev/null +++ b/test/schema/test_types.py @@ -0,0 +1,225 @@ +""" + +Part of the tagit test suite. +A copy of the license is provided with the project. +Author: Matthias Baumgartner, 2022 +""" +# imports +import operator +import unittest + +# bsfs imports +from bsfs.namespace import ns +from bsfs.utils import errors + +# objects to test +from bsfs.schema.types import _Type, _Vertex, Node, Literal, Predicate + + +## code ## + +class TestType(unittest.TestCase): + def test_parents(self): + # create some types + fst = _Type('First') + snd = _Type('Second', fst) + trd = _Type('Third', snd) + frd = _Type('Fourth', trd) + # check parents + self.assertListEqual(list(fst.parents()), []) + self.assertListEqual(list(snd.parents()), [fst]) + self.assertListEqual(list(trd.parents()), [snd, fst]) + self.assertListEqual(list(frd.parents()), [trd, snd, fst]) + + def test_essentials(self): + # type w/o parent + self.assertEqual(str(_Type('Foo')), '_Type(Foo)') + self.assertEqual(repr(_Type('Foo')), '_Type(Foo, None)') + # type w/ parent + self.assertEqual(str(_Type('Foo', _Type('Bar'))), '_Type(Foo)') + self.assertEqual(repr(_Type('Foo', _Type('Bar'))), '_Type(Foo, _Type(Bar, None))') + # subtype w/o parent + class SubType(_Type): pass + self.assertEqual(str(SubType('Foo')), 'SubType(Foo)') + self.assertEqual(repr(SubType('Foo')), 'SubType(Foo, None)') + # subtype w/ parent + self.assertEqual(str(SubType('Foo', SubType('Bar'))), 'SubType(Foo)') + self.assertEqual(repr(SubType('Foo', SubType('Bar'))), 'SubType(Foo, SubType(Bar, None))') + # subtype and type mixed + self.assertEqual(str(SubType('Foo', _Type('Bar'))), 'SubType(Foo)') + self.assertEqual(repr(SubType('Foo', _Type('Bar'))), 'SubType(Foo, _Type(Bar, None))') + 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): + # callee is used as parent + self.assertEqual(_Type('First').get_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')))) + # type persists + class Foo(_Type): pass + self.assertEqual(Foo('First').get_child('Second'), Foo('Second', Foo('First'))) + + def test_equality(self): + # equality depends on uri + self.assertEqual(_Type('Foo'), _Type('Foo')) + self.assertEqual(hash(_Type('Foo')), hash(_Type('Foo'))) + self.assertNotEqual(_Type('Foo'), _Type('Bar')) + self.assertNotEqual(hash(_Type('Foo')), hash(_Type('Bar'))) + # comparison is case-sensitive + self.assertNotEqual(_Type('FOO'), _Type('foo')) + self.assertNotEqual(hash(_Type('FOO')), hash(_Type('foo'))) + # comparison respects type + class Foo(_Type): pass + self.assertNotEqual(_Type('Foo'), Foo('Foo')) + self.assertNotEqual(hash(_Type('Foo')), hash(Foo('Foo'))) + # comparison respects parent + self.assertNotEqual(_Type('Foo', _Type('Bar')), _Type('Foo')) + self.assertNotEqual(hash(_Type('Foo', _Type('Bar'))), hash(_Type('Foo'))) + + def test_order(self): + # create some types. + vehicle = _Type('Vehicle') + twowheel = _Type('Two-wheel', vehicle) + bike = _Type('Bike', twowheel) + bicycle = _Type('Bicycle', twowheel) + # two-wheel is equivalent to itself + self.assertFalse(twowheel == vehicle) + self.assertTrue(twowheel == twowheel) + self.assertFalse(twowheel == bicycle) + # two-wheel is a true subclass of Vehicle + self.assertTrue(twowheel < vehicle) + self.assertFalse(twowheel < twowheel) + self.assertFalse(twowheel < bicycle) + # two-wheel is a subclass of itself and Vehicle + self.assertTrue(twowheel <= vehicle) + self.assertTrue(twowheel <= twowheel) + self.assertFalse(twowheel <= bicycle) + # two-wheel is a true superclass of Bicycle + self.assertFalse(twowheel > vehicle) + self.assertFalse(twowheel > twowheel) + self.assertTrue(twowheel > bicycle) + # two-wheel is a superclass of itself and Bicycle + self.assertFalse(twowheel >= vehicle) + self.assertTrue(twowheel >= twowheel) + self.assertTrue(twowheel >= bicycle) + # analoguous to sets, this is not a total order + self.assertFalse(bike <= bicycle) + self.assertFalse(bike < bicycle) + self.assertFalse(bike > bicycle) + self.assertFalse(bike >= bicycle) + self.assertFalse(bike == bicycle) + 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) + # 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) + +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 + 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) + class Foo(): pass + self.assertRaises(TypeError, Predicate, ns.bse.foo, None, Node(ns.bsfs.Node, None), Foo(), True) + + def test_equality(self): + 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, + domain=n_root, + range=None, + unique=False, + ) + # instance is equal to itself + self.assertEqual(root, root) + self.assertEqual(hash(root), hash(root)) + # 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))) + # 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))) + # 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))) + # 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))) + # 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))) + # 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))) + + def test_get_child(self): + 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, + domain=n_root, + range=None, + unique=False, + ) + tag = Predicate( + uri=ns.bsfs.Entity, + parent=root, + domain=n_ent, + range=n_tag, + unique=False, + ) + + # uri is respected + self.assertEqual(ns.bse.foo, tag.get_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) + # range is respected + rng = Node(ns.bsfs.Group, n_tag) + self.assertEqual(rng, tag.get_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) + # unique is respected + self.assertTrue(tag.get_child(ns.bse.foo, unique=True).unique) + + # domain is inherited from parent + self.assertEqual(n_ent, tag.get_child(ns.bse.foo).domain) + # range is inherited from parent + 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) + + # 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)) + + +## main ## + +if __name__ == '__main__': + unittest.main() + +## EOF ## + |