aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--bsfs/schema/__init__.py24
-rw-r--r--bsfs/schema/schema.py325
-rw-r--r--bsfs/schema/types.py269
-rw-r--r--test/schema/__init__.py0
-rw-r--r--test/schema/test_schema.py616
-rw-r--r--test/schema/test_types.py225
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 ##
+