diff options
Diffstat (limited to 'bsfs/schema')
-rw-r--r-- | bsfs/schema/schema.py | 56 | ||||
-rw-r--r-- | bsfs/schema/types.py | 65 |
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, ): |