aboutsummaryrefslogtreecommitdiffstats
path: root/bsfs/schema
diff options
context:
space:
mode:
Diffstat (limited to 'bsfs/schema')
-rw-r--r--bsfs/schema/schema.py56
-rw-r--r--bsfs/schema/types.py65
2 files changed, 77 insertions, 44 deletions
diff --git a/bsfs/schema/schema.py b/bsfs/schema/schema.py
index b6f37a7..c5d4571 100644
--- a/bsfs/schema/schema.py
+++ b/bsfs/schema/schema.py
@@ -25,11 +25,28 @@ __all__: typing.Sequence[str] = (
## code ##
class Schema():
- """
+ """Graph schema.
+
+ Use `Schema.Empty()` to create a new, empty Schema rather than construct
+ it directly.
+
+ The schema is defined by three sets: Predicates, Nodes, and Literals.
+
+ The Schema class guarantees two properties: completeness and consistency.
+ Completeness means that the schema covers all class that are referred to
+ by any other class in the schema. Consistency means that each class is
+ identified by a unique URI and all classes that use that URI consequently
+ use the same definition.
+
"""
+ # node classes.
_nodes: typing.Dict[URI, types.Node]
+
+ # literal classes.
_literals: typing.Dict[URI, types.Literal]
+
+ # predicate classes.
_predicates: typing.Dict[URI, types.Predicate]
def __init__(
@@ -47,7 +64,8 @@ class Schema():
literals = set(literals)
predicates = set(predicates)
# include parents in predicates set
- predicates |= {par for pred in predicates for par in pred.parents()}
+ # TODO: review type annotations and ignores for python >= 3.11 (parents is _Type but should be typing.Self)
+ predicates |= {par for pred in predicates for par in pred.parents()} # type: ignore [misc]
# include predicate domain in nodes set
nodes |= {pred.domain for pred in predicates}
# include predicate range in nodes and literals sets
@@ -57,8 +75,8 @@ class Schema():
# 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()}
+ nodes |= {par for node in nodes for par in node.parents()} # type: ignore [misc]
+ literals |= {par for lit in literals for par in lit.parents()} # type: ignore [misc]
# assign members
self._nodes = {node.uri: node for node in nodes}
self._literals = {lit.uri: lit for lit in literals}
@@ -153,9 +171,7 @@ class Schema():
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.
- """
+ """Checks if two schemas have different predicate, node, or literal definitions for the same uri."""
# check arg
if not isinstance(other, Schema):
raise TypeError(other)
@@ -181,7 +197,10 @@ class Schema():
return True
@classmethod
- def Union(cls, *args: typing.Union['Schema', typing.Iterable['Schema']]) -> 'Schema':
+ def Union( # pylint: disable=invalid-name # capitalized classmethod
+ 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.
@@ -200,7 +219,7 @@ class Schema():
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]
+ args = args[0] # type: ignore [assignment] # we checked and thus know that args[0] is an iterable
else:
raise TypeError(f'expected multiple Schema instances or a single Iterable, found {args}')
@@ -237,25 +256,31 @@ class Schema():
## getters ##
- # FIXME: which of the getters below are actually needed?
+ # FIXME: nodes, predicates, literals could be properties
# FIXME: interchangeability of URI and _Type?!
def has_node(self, node: URI) -> bool:
+ """Return True if a Node with URI *node* is part of the schema."""
return node in self._nodes
def has_literal(self, lit: URI) -> bool:
+ """Return True if a Literal with URI *lit* is part of the schema."""
return lit in self._literals
def has_predicate(self, pred: URI) -> bool:
+ """Return True if a Predicate with URI *pred* is part of the schema."""
return pred in self._predicates
- def nodes(self) -> typing.Iterator[types.Node]: # FIXME: type annotation
+ def nodes(self) -> typing.Iterable[types.Node]:
+ """Return an iterator over Node classes."""
return self._nodes.values()
- def literals(self) -> typing.Iterator[types.Literal]: # FIXME: type annotation
+ def literals(self) -> typing.Iterable[types.Literal]:
+ """Return an iterator over Literal classes."""
return self._literals.values()
- def predicates(self) -> typing.Iterator[types.Predicate]: # FIXME: type annotation
+ def predicates(self) -> typing.Iterable[types.Predicate]:
+ """Return an iterator over Predicate classes."""
return self._predicates.values()
def node(self, uri: URI) -> types.Node:
@@ -275,7 +300,8 @@ class Schema():
@classmethod
- def Empty(cls) -> 'Schema':
+ def Empty(cls) -> 'Schema': # pylint: disable=invalid-name # capitalized classmethod
+ """Return a minimal Schema."""
node = types.Node(ns.bsfs.Node, None)
literal = types.Literal(ns.bsfs.Literal, None)
predicate = types.Predicate(
@@ -289,7 +315,7 @@ class Schema():
@classmethod
- def from_string(cls, schema: str) -> 'Schema':
+ def from_string(cls, schema: str) -> 'Schema': # pylint: disable=invalid-name # capitalized classmethod
"""Load and return a Schema from a string."""
# parse string into rdf graph
graph = rdflib.Graph()
diff --git a/bsfs/schema/types.py b/bsfs/schema/types.py
index 6e257e3..54a7e99 100644
--- a/bsfs/schema/types.py
+++ b/bsfs/schema/types.py
@@ -93,7 +93,7 @@ class _Type():
uri: URI
# parent's class uris.
- parent: typing.Optional['_Type']
+ parent: typing.Optional['_Type'] # TODO: for python >=3.11: use typing.Self
def __init__(
self,
@@ -123,63 +123,70 @@ class _Type():
def __hash__(self) -> int:
return hash((type(self), self.uri, self.parent))
+ # NOTE: For equality and order functions (lt, gt, le, ge) we explicitly want type equality!
+ # Consider the statements below, with class Vehicle(_Type) and class TwoWheel(Vehicle):
+ # * Vehicle('foo', None) == TwoWheel('foo', None): Instances of different types cannot be equivalent.
+ # * Vehicle('foo', None) <= TwoWheel('foo', None): Cannot compare the different types Vehicles and TwoWheel.
+
def __eq__(self, other: typing.Any) -> bool:
"""Return True iff *self* is equivalent to *other*."""
- return type(self) == type(other) \
+ # pylint: disable=unidiomatic-typecheck
+ return type(other) is type(self) \
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
+ if not type(self) is type(other): # type mismatch # pylint: disable=unidiomatic-typecheck
return NotImplemented
- elif self.uri == other.uri: # equivalence
+ if self.uri == other.uri: # equivalence
return False
- elif self in other.parents(): # superclass
+ if self in other.parents(): # superclass
return False
- elif other in self.parents(): # subclass
+ if other in self.parents(): # subclass
return True
- else: # not related
- return False
+ # 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
+ if not type(self) is type(other): # type mismatch # pylint: disable=unidiomatic-typecheck
return NotImplemented
- elif self.uri == other.uri: # equivalence
+ if self.uri == other.uri: # equivalence
return True
- elif self in other.parents(): # superclass
+ if self in other.parents(): # superclass
return False
- elif other in self.parents(): # subclass
+ if other in self.parents(): # subclass
return True
- else: # not related
- return False
+ # 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
+ if not type(self) is type(other): # type mismatch # pylint: disable=unidiomatic-typecheck
return NotImplemented
- elif self.uri == other.uri: # equivalence
+ if self.uri == other.uri: # equivalence
return False
- elif self in other.parents(): # superclass
+ if self in other.parents(): # superclass
return True
- elif other in self.parents(): # subclass
- return False
- else: # not related
+ if other in self.parents(): # subclass
return False
+ # 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
+ if not type(self) is type(other): # type mismatch # pylint: disable=unidiomatic-typecheck
return NotImplemented
- elif self.uri == other.uri: # equivalence
+ if self.uri == other.uri: # equivalence
return True
- elif self in other.parents(): # superclass
+ if self in other.parents(): # superclass
return True
- elif other in self.parents(): # subclass
- return False
- else: # not related
+ if other in self.parents(): # subclass
return False
+ # not related
+ return False
class _Vertex(_Type):
@@ -216,10 +223,10 @@ class Predicate(_Type):
self,
# Type members
uri: URI,
- parent: 'Predicate',
+ parent: typing.Optional['Predicate'],
# Predicate members
domain: Node,
- range: typing.Optional[typing.Union[Node, Literal]],
+ range: typing.Optional[typing.Union[Node, Literal]], # pylint: disable=redefined-builtin
unique: bool,
):
# check arguments
@@ -246,7 +253,7 @@ class Predicate(_Type):
self,
uri: URI,
domain: typing.Optional[Node] = None,
- range: typing.Optional[_Vertex] = None,
+ range: typing.Optional[_Vertex] = None, # pylint: disable=redefined-builtin
unique: typing.Optional[bool] = None,
**kwargs,
):