From 6a04bec96a2aba791e65d4a45a7e80a0ba8717b5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Manuel=20Dom=C3=ADnguez?= Date: Fri, 9 Sep 2022 18:20:58 +0200 Subject: [PATCH 1/7] Fix docstrings and public API definitions --- simphony_osp/ontology/composition.py | 8 +- simphony_osp/ontology/entity.py | 216 ++++++++------- simphony_osp/ontology/individual.py | 100 +++---- simphony_osp/ontology/namespace.py | 122 ++++---- simphony_osp/ontology/oclass.py | 87 +++--- simphony_osp/ontology/restriction.py | 38 ++- simphony_osp/session/session.py | 400 +++++++++++++++------------ simphony_osp/tools/general.py | 9 +- simphony_osp/tools/import_export.py | 8 + simphony_osp/tools/pico.py | 7 +- simphony_osp/tools/pretty_print.py | 14 +- simphony_osp/tools/search.py | 7 +- simphony_osp/tools/semantic2dot.py | 21 +- tests/test_api.py | 8 +- 14 files changed, 575 insertions(+), 470 deletions(-) diff --git a/simphony_osp/ontology/composition.py b/simphony_osp/ontology/composition.py index 4dd50534..35d7d012 100644 --- a/simphony_osp/ontology/composition.py +++ b/simphony_osp/ontology/composition.py @@ -27,7 +27,7 @@ class OPERATOR(Enum): class Composition(OntologyEntity): - """Combine multiple classes using logical formulae.""" + """Combinations of multiple classes using logical formulae.""" rdf_type = OWL.Class rdf_identifier = BNode @@ -57,6 +57,9 @@ def operands( _, operands = self._get_operator_and_operands return tuple(operands) + # ↑ ------ ↑ + # Public API + def __str__(self) -> str: """Transform to a Protege-like string.""" s = f" {self.operator} ".join(map(str, self.operands)) @@ -64,9 +67,6 @@ def __str__(self) -> str: s = f"{self.operator} {s}" return f"({s})" - # ↑ ------ ↑ - # Public API - def __init__( self, uid: UID, diff --git a/simphony_osp/ontology/entity.py b/simphony_osp/ontology/entity.py index 9b65dc8e..c36e8ea3 100644 --- a/simphony_osp/ontology/entity.py +++ b/simphony_osp/ontology/entity.py @@ -33,7 +33,7 @@ class OntologyEntity(ABC): - """Abstract superclass of any entity in the ontology.""" + """Abstract superclass of any entity in ontology entity.""" rdf_type: Optional[Union[URIRef, Set[URIRef]]] = None rdf_identifier: Type @@ -59,16 +59,6 @@ def identifier(self) -> Identifier: """ return self.uid.to_identifier() - @property - def uid(self) -> UID: - """Get a SimPhoNy identifier for this entity. - - The SimPhoNy identifier is known as UID. An UID is a Python class - defined in SimPhoNy and can always be converted to a semantic web - identifier. - """ - return self._uid - @property def label(self) -> Optional[str]: """Get the preferred label of this entity, if it exists. @@ -98,20 +88,20 @@ def label(self, value: str) -> None: @property def label_lang(self) -> Optional[str]: - """Get the language of the preferred label of this entity. + """Get the language of the main label of this entity. See the docstring for `label_literal` for more information on the - definition of preferred label. + definition of main label. """ label_literal = self.label_literal return label_literal.language if label_literal is not None else None @label_lang.setter def label_lang(self, value: str) -> None: - """Set the language of the preferred label of this entity. + """Set the language of the main label of this entity. See the docstring for `label_literal` for more information on the - definition of preferred label. + definition of main label. """ self.label_literal = Literal(self.label_literal, lang=value) @@ -166,7 +156,7 @@ def superclasses(self: ONTOLOGY_ENTITY) -> FrozenSet[ONTOLOGY_ENTITY]: """Get the superclass of the entity. Returns: - The direct superclasses of the entity. + The superclasses of the entity. """ return frozenset(self._get_superclasses()) @@ -177,7 +167,7 @@ def subclasses(self: ONTOLOGY_ENTITY) -> FrozenSet[ONTOLOGY_ENTITY]: """Get the subclasses of the entity. Returns: - The direct subclasses of the entity + The subclasses of the entity """ return frozenset(self._get_subclasses()) @@ -205,34 +195,17 @@ def is_subclass_of(self, other: OntologyEntity) -> bool: """ return self in other.subclasses - def __str__(self) -> str: - """Transform the entity into a human-readable string.""" - return ( - f"{self.label}" - if hasattr(self, "label") and self.label is not None - else f"{self._uid}" - ) - - def __repr__(self) -> str: - """Transform the entity into a string.""" - header = f"{self.__class__.__name__}" - elements = [ - f"{self.label}" - if hasattr(self, "label") and self.label is not None - else None, - f"{self.uid}", - ] - elements = filter(lambda x: x is not None, elements) - return f"<{header}: {' '.join(elements)}>" - def __eq__(self, other: OntologyEntity) -> bool: """Check whether two entities are the same. + Two entities are considered equal when they have the same identifier + and are stored in the same session. + Args: other: The other entity. Returns: - bool: Whether the two entities are the same. + Whether the two entities are the same. """ # TODO: Blank nodes with different IDs. return ( @@ -241,24 +214,45 @@ def __eq__(self, other: OntologyEntity) -> bool: and self.identifier == other.identifier ) - def __hash__(self) -> int: - """Make the entity hashable.""" - return hash((self._uid, self.session)) - def __bool__(self): """Returns the boolean value of the entity, always true.""" return True - # ↑ ------ ↑ - # Public API + def iter_labels( + self, + lang: Optional[str] = None, + return_prop: bool = False, + return_literal: bool = True, + ) -> Iterator[ + Union[Literal, str, Tuple[str, URIRef], Tuple[Literal, URIRef]] + ]: + """Returns all the available labels for this ontology entity. + + Args: + lang: retrieve labels only in a specific language. + return_prop: Whether to return the property that designates the + label. When active, it is the second argument. + return_literal: Whether to return a literal or a string with the + label (the former contains the language, the latter not). + + Returns: + An iterator yielding strings or literals; or tuples whose first + element is a string or literal, and second element the property + defining this label. + """ + return self.session.iter_labels( + entity=self, + lang=lang, + return_literal=return_literal, + return_prop=return_prop, + ) @property def label_literal(self) -> Optional[Literal]: - """Get the preferred label for this entity. + """Get the main label for this entity. - The labels are first sorted by the property defining them (which is - an attribute of the session that this entity is stored on), and then by - their length. + The labels are first sorted by the property defining them, then by + their language, and then by their length. Returns: The first label in the resulting ordering is returned. If the @@ -271,41 +265,97 @@ def label_literal(self) -> Optional[Literal]: @label_literal.setter def label_literal(self, value: Optional[Literal]) -> None: - """Replace the preferred label for this entity. + """Replace the main label for this entity. The labels are first sorted by the property defining them (which is an attribute of the session that this entity is stored on), and then by their length. Args: - value: the preferred label to replace the current one with. If + value: the main label to replace the current one with. If None, then all labels for this entity are deleted. """ labels = self.iter_labels(return_literal=True, return_prop=True) labels = self._sort_labels_and_properties_by_preference(labels) - preferred_label = labels[0] if len(labels) > 0 else None + main_label = labels[0] if len(labels) > 0 else None # Label deletion. if value is None: - for label_prop in self.session.label_properties: + for label_prop in self.session.label_predicates: self.session.graph.remove((self.identifier, label_prop, None)) - elif preferred_label is not None: + elif main_label is not None: self.session.graph.remove( - (self.identifier, preferred_label[1], preferred_label[0]) + (self.identifier, main_label[1], main_label[0]) ) # Label creation. if value is not None: - if preferred_label is not None: - self.session.graph.add( - (self.identifier, preferred_label[1], value) - ) + if main_label is not None: + self.session.graph.add((self.identifier, main_label[1], value)) else: self.session.graph.add( - (self.identifier, self.session.label_properties[0], value) + (self.identifier, self.session.label_predicates[0], value) ) + @property + def triples(self) -> Set[Triple]: + """Get the all the triples where the entity is the subject. + + Triples from the underlying RDFLib graph where the entity is stored + in which the entity's identifier is the subject. + """ + if self.__graph is not None: + return set(self.__graph.triples((None, None, None))) + else: + return set( + self.session.graph.triples((self.identifier, None, None)) + ) + + # ↑ ------ ↑ + # Public API + + @property + def uid(self) -> UID: + """Get a SimPhoNy identifier for this entity. + + The SimPhoNy identifier is known as UID. An UID is a Python class + defined in SimPhoNy and can always be converted to a semantic web + identifier. + """ + return self._uid + + @property + def graph(self) -> Graph: + """Graph where the ontology entity's data lives.""" + return self.session.graph if self.session is not None else self.__graph + + __graph: Optional[Graph] = None # Only exists during initialization. + + def __hash__(self) -> int: + """Make the entity hashable.""" + return hash((self._uid, self.session)) + + def __str__(self) -> str: + """Transform the entity into a human-readable string.""" + return ( + f"{self.label}" + if hasattr(self, "label") and self.label is not None + else f"{self._uid}" + ) + + def __repr__(self) -> str: + """Transform the entity into a string.""" + header = f"{self.__class__.__name__}" + elements = [ + f"{self.label}" + if hasattr(self, "label") and self.label is not None + else None, + f"{self.uid}", + ] + elements = filter(lambda x: x is not None, elements) + return f"<{header}: {' '.join(elements)}>" + def _sort_labels_and_properties_by_preference( self, labels: Iterator[Tuple[Literal, URIRef]] ) -> List[Tuple[Literal, URIRef]]: @@ -324,7 +374,7 @@ def _sort_labels_and_properties_by_preference( labels = sorted( labels, key=lambda x: ( - self.session.label_properties.index(x[1]), + self.session.label_predicates.index(x[1]), ( self.session.label_languages + ("en", None, x[0].language) ).index(x[0].language), @@ -333,50 +383,6 @@ def _sort_labels_and_properties_by_preference( ) return labels - def iter_labels( - self, - lang: Optional[str] = None, - return_prop: bool = False, - return_literal: bool = True, - ) -> Iterator[ - Union[Literal, str, Tuple[str, URIRef], Tuple[Literal, URIRef]] - ]: - """Returns all the available labels for this ontology entity. - - Args: - lang: retrieve labels only in a specific language. - return_prop: Whether to return the property that designates the - label. When active, it is the second argument. - return_literal: Whether to return a literal or a string with the - label (the former contains the language, the latter not). - - Returns: - An iterator yielding strings or literals; or tuples whose first - element is a string or literal, and second element the property - defining this label. - """ - return self.session.iter_labels( - entity=self, - lang=lang, - return_literal=return_literal, - return_prop=return_prop, - ) - - @property - def triples(self) -> Set[Triple]: - """Get the all the triples where the entity is the subject.""" - if self.__graph is not None: - return set(self.__graph.triples((None, None, None))) - else: - return set( - self.session.graph.triples((self.identifier, None, None)) - ) - - @property - def graph(self) -> Graph: - """Graph where the ontology entity's data lives.""" - return self.session.graph if self.session is not None else self.__graph - @abstractmethod def _get_direct_superclasses( self: ONTOLOGY_ENTITY, @@ -401,8 +407,6 @@ def _get_subclasses(self: ONTOLOGY_ENTITY) -> Iterable[ONTOLOGY_ENTITY]: """Subclass getter specific to the type of ontology entity.""" pass - __graph: Optional[Graph] = None # Only exists during initialization. - @abstractmethod def __init__( self, diff --git a/simphony_osp/ontology/individual.py b/simphony_osp/ontology/individual.py index 5ecfb46d..bf3b4d10 100644 --- a/simphony_osp/ontology/individual.py +++ b/simphony_osp/ontology/individual.py @@ -206,19 +206,9 @@ def __init__( class AttributeSet(ObjectSet): """A set interface to an ontology individual's attributes. - This class looks like and acts like the standard `set`, but it - is an interface to the `attributes_add`, attributes_set`, - `attributes_delete`, `attributes_value_contains` and - `attributes_value_generator` methods. - - When an instance is read, the methods `attributes_value_generator` - and `attributes_value_contains` are used to fetch the data. When it - is modified in-place, the methods `attributes_add`, `attributes_set`, - and `attributes_delete` are used to reflect the changes. - - This class does not hold any attribute-related information itself, thus - it is safe to spawn multiple instances linked to the same attribute - and ontology individual (when single-threading). + This class looks like and acts like the standard `set`, but it is an + interface to the methods from `OntologyIndividual` that manage the + attributes. """ # Public API @@ -246,7 +236,7 @@ def __iter__(self) -> Iterator[AttributeValue]: yield value def __contains__(self, item: AttributeValue) -> bool: - """Check whether a value is assigned to the attribute.""" + """Check whether a value is assigned to the set's attribute.""" return any( self._individual.attributes_value_contains(attribute, item) for attribute in self._predicates @@ -320,18 +310,9 @@ def __init__( class RelationshipSet(ObjectSet): """A set interface to an ontology individual's relationships. - This class looks like and acts like the standard `set`, but it - is an interface to the `relationships_connect`, `relationships_disconnect` - and `relationships_iter` methods. - - When an instance is read, the method `relationships_iter` is used to fetch - the data. When it is modified in-place, the methods - `relationships_connect` and `relationships_disconnect` are used to - reflect the changes. - - This class does not hold any relationship-related information itself, - thus it is safe to spawn multiple instances linked to the same - relationship and ontology individual (when single-threading). + This class looks like and acts like the standard `set`, but it is an + interface to the methods from `OntologyIndividual` that manage the + relationships. """ @staticmethod @@ -433,7 +414,7 @@ def __iter__(self) -> Iterator[OntologyIndividual]: ) def __contains__(self, item: OntologyIndividual) -> bool: - """Check if an individual is connected via the relationship.""" + """Check if an individual is connected via set's relationship.""" if item not in self.individual.session: return False @@ -750,10 +731,6 @@ class AnnotationSet(ObjectSet): This class looks like and acts like the standard `set`, but it is an interface to the methods from `OntologyIndividual` that manage the annotations. - - This class does not hold any annotation-related information itself, - thus it is safe to spawn multiple instances linked to the same - relationship and ontology individual (when single-threading). """ _predicate: OntologyAnnotation @@ -858,6 +835,9 @@ def __init__( def classes(self) -> FrozenSet[OntologyClass]: """Get the ontology classes of this ontology individual. + This property is writable. The classes that an ontology individual + belongs to can be changed writing the desired values to this property. + Returns: A set with all the ontology classes of the ontology individual. When the individual has no classes, the set is empty. @@ -894,9 +874,9 @@ def is_a(self, ontology_class: OntologyClass) -> bool: Returns: Whether the ontology individual is an instance of such ontology - class. + class. """ - return any(oc in ontology_class.subclasses for oc in self.classes) + return self.is_subclass_of(ontology_class) def __dir__(self) -> Iterable[str]: """Show the individual's attributes as autocompletion suggestions. @@ -979,7 +959,7 @@ def __setattr__( Args: name: The label or suffix of the attribute. - value: The new value. + value: The new value(s). Raises: AttributeError: Unknown attribute label or suffix. @@ -1284,7 +1264,7 @@ def __setitem__( ) def __delitem__(self, rel: OntologyPredicate): - """Delete all objects attached through rel. + """Delete all objects attached through the given predicate. Args: rel: Either an ontology attribute, an ontology relationship or @@ -1303,8 +1283,8 @@ def connect( """Connect ontology individuals to other ontology individuals. Args: - individuals: The objects to be added. Their identifiers may also - be used. + individuals: The individuals to be connected. Their identifiers may + also be used. rel: The relationship between the objects. Raises: @@ -1431,6 +1411,10 @@ def get( the call. See the "Returns:" section of this docstring for more details on this. + Note: If you are reading the SimPhoNy documentation API Reference, it + is likely that you cannot read this docstring. As a workaround, click + the `source` button to read it in its raw form. + Args: individuals: Restrict the elements to be returned to a certain subset of the connected elements. @@ -1568,6 +1552,10 @@ def iter( the call. See the "Returns:" section of this docstring for more details on this. + Note: If you are reading the SimPhoNy documentation API Reference, it + is likely that you cannot read this docstring. As a workaround, click + the `source` button to read it in its raw form. + Args: individuals: Restrict the elements to be returned to a certain subset of the connected elements. @@ -1697,6 +1685,31 @@ def operations(self) -> OperationsNamespace: self._operations_namespace = OperationsNamespace(individual=self) return self._operations_namespace + @property + def attributes( + self, + ) -> Mapping[OntologyAttribute, FrozenSet[AttributeValue]]: + """Get the attributes of this individual as a dictionary.""" + generator = self.attributes_attribute_and_value_generator() + return MappingProxyType( + {attr: frozenset(gen) for attr, gen in generator} + ) + + def is_subclass_of(self, ontology_class: OntologyEntity) -> bool: + """Check if the individual is an instance of the given ontology class. + + Args: + ontology_class: The ontology class to test against. + + Returns: + Whether the ontology individual is an instance of such ontology + class. + """ + return bool(set(self.classes) & set(ontology_class.subclasses)) + + # ↑ ------ ↑ + # Public API + def __enter__(self) -> ContainerEnvironment: """Use an ontology individual as a context manager. @@ -1742,19 +1755,6 @@ def __exit__(self, *args): raise AttributeError("__exit__") - @property - def attributes( - self, - ) -> Mapping[OntologyAttribute, FrozenSet[AttributeValue]]: - """Get the attributes of this individual as a dictionary.""" - generator = self.attributes_attribute_and_value_generator() - return MappingProxyType( - {attr: frozenset(gen) for attr, gen in generator} - ) - - # ↑ ------ ↑ - # Public API - _operations_namespace: Optional[OperationsNamespace] = None """Holds the operations namespace instance for this ontology individual. diff --git a/simphony_osp/ontology/namespace.py b/simphony_osp/ontology/namespace.py index 08c25e24..baea1cb5 100644 --- a/simphony_osp/ontology/namespace.py +++ b/simphony_osp/ontology/namespace.py @@ -20,34 +20,23 @@ class OntologyNamespace: - """An ontology namespace.""" + """An ontology namespace. + + Ontology namespace objects allow access to the terminological knowledge + from the installed ontologies. + """ # Public API # ↓ ------ ↓ - def __init__( - self, - iri: Union[str, URIRef], - ontology: Optional[Session] = None, - name: Optional[str] = None, - ): - """Initialize a namespace object. - - Args: - iri: The IRI of the namespace. - ontology: The ontology to which the namespace is connected. - name: The name of the namespace - """ - from simphony_osp.session.session import Session - - ontology = ontology or Session.get_default_session() - self._iri = URIRef(iri) - self._ontology = ontology - ontology.bind(name, iri) - @property def name(self) -> Optional[str]: - """The name of the namespace.""" + """The name of the namespace. + + For namespaces that have been imported from the + `simphony_osp.namespaces` module, this name matches the alias given to + the namespace in its ontology package. + """ return self.ontology.get_namespace_bind(self) @property @@ -55,14 +44,12 @@ def iri(self) -> URIRef: """The IRI of the namespace.""" return self._iri - @property - def ontology(self) -> Session: - """Returns the session that the namespace is bound to.""" - return self._ontology - def __eq__(self, other: OntologyNamespace) -> bool: """Check whether the two namespace objects are equal. + Two namespace objects are considered to be equal when both have the + same IRI and are bound to the same session. + Args: other: The namespace object to compare with. @@ -75,16 +62,8 @@ def __eq__(self, other: OntologyNamespace) -> bool: and self.iri == other.iri ) - def __hash__(self) -> int: - """Hash the namespace. - - The namespace is defined by its IRI and its underlying data - structure (the ontology), which are immutable attributes. - """ - return hash((self.ontology, self.iri)) - def __getattr__(self, name: str) -> OntologyEntity: - """Get an entity from the ontology associated to the namespace. + """Retrieve an entity by suffix or label. Args: name: The label or namespace suffix of the ontology entity. @@ -101,14 +80,14 @@ def __getattr__(self, name: str) -> OntologyEntity: except KeyError as e: raise AttributeError(str(e)) from e - def __getitem__(self, label: str) -> OntologyEntity: - """Get an entity from the ontology associated to the namespace. + def __getitem__(self, name: str) -> OntologyEntity: + """Retrieve an entity by suffix or label. Useful for entities whose labels or suffixes contain characters which - are not compatible with the Python syntax. + are not compatible with the Python syntax rules. Args: - label: The label of the ontology entity. + name: The suffix or label of the ontology entity. Raises: KeyError: Unknown label or suffix. @@ -117,14 +96,14 @@ def __getitem__(self, label: str) -> OntologyEntity: Returns: An ontology entity with matching label or suffix. """ - if not isinstance(label, str): + if not isinstance(name, str): exception = TypeError( f"{str(type(self)).capitalize()} indices must be" f"of type {str}." ) raise exception - return self.get(label) + return self.get(name) def __dir__(self) -> Iterable[str]: """Attributes available for the OntologyNamespace class. @@ -159,13 +138,19 @@ def __iter__(self) -> Iterator[OntologyEntity]: return (entity for entity in iter(self.ontology) if entity in self) def __contains__(self, item: Union[OntologyEntity, Identifier]) -> bool: - """Check whether the given entity is part of the namespace. + """Check whether the given ontology entity is part of the namespace. + + An ontology entity is considered to be part of a namespace if its IRI + starts with the namespace IRI and if it is part of the session that + the namespace is bound to. Identifiers are only required to start with + the namespace IRI to be considered part of the namespace object. Blank + nodes are never part of a namespace. Args: item: An ontology entity or identifier. Returns: - Whether the given entity name or IRI is part of the namespace. + Whether the given entity or identifier is part of the namespace. Blank nodes are never part of a namespace. """ if isinstance(item, Identifier) and not isinstance(item, URIRef): @@ -193,14 +178,15 @@ def __len__(self) -> int: lambda self: self.ontology.entity_cache_timestamp, maxsize=4096 ) def get(self, name: str, default: Optional[Any] = None) -> OntologyEntity: - """Get ontology entities from the registry by suffix or label. + """Get ontology entities from the bounded session by suffix or label. Args: name: The label or suffix of the ontology entity. - default: The value to return if no entity is found. + default: The entity to return if no entity with such label or + suffix is found. Raises: - KeyError: Unknown label or suffix. + KeyError: Unknown label or suffix (and no default given). KeyError: Multiple entities for the given label or suffix. Returns: @@ -335,6 +321,40 @@ def from_label( # ↑ ------ ↑ # Public API + @property + def ontology(self) -> Session: + """Returns the session that the namespace is bound to. + + Retrieving entities from this namespace object actually implies + retrieving them from such session. Namespace objects imported from the + module `simphony_osp.namespaces` are bound to the "default ontology" + session, which contains all the ontology entities from the ontologies + that were installed using pico. + """ + return self._ontology + + def __init__( + self, + iri: Union[str, URIRef], + ontology: Optional[Session] = None, + name: Optional[str] = None, + ): + """Initialize a namespace object. + + Args: + iri: The IRI of the namespace. + ontology: The session that the namespace object is bound to (see + the docstring of the `ontology` property). + name: The name of the namespace (see the docstring of the `name` + property). + """ + from simphony_osp.session.session import Session + + ontology = ontology or Session.get_default_session() + self._iri = URIRef(iri) + self._ontology = ontology + ontology.bind(name, iri) + def __str__(self) -> str: """Transform the namespace to a human-readable string. @@ -351,6 +371,14 @@ def __repr__(self) -> str: """ return f"<{self.name}: {self.iri}>" + def __hash__(self) -> int: + """Hash the namespace. + + The namespace is defined by its IRI and its underlying data + structure (the ontology), which are immutable attributes. + """ + return hash((self.ontology, self.iri)) + @lru_cache_timestamp( lambda self: self.ontology.entity_cache_timestamp, maxsize=4096 ) diff --git a/simphony_osp/ontology/oclass.py b/simphony_osp/ontology/oclass.py index 02bd4205..663b0ea5 100644 --- a/simphony_osp/ontology/oclass.py +++ b/simphony_osp/ontology/oclass.py @@ -69,8 +69,8 @@ def attributes( """Get the attributes of this class. The attributes that all instances of this class are expected to - have. A class can have attributes because one of its superclasses ( - including itself) has a default value for an attribute, or because + have. A class can have attributes because one of its superclasses + (including itself) has a default value for an attribute, or because the axioms affecting the superclass explicitly state that the class has such an attribute. """ @@ -91,10 +91,31 @@ def attributes( return MappingProxyType(attributes) @property - def axioms(self) -> FrozenSet[Restriction]: + @lru_cache_timestamp(lambda self: self.session.entity_cache_timestamp) + def optional_attributes(self) -> FrozenSet[OntologyAttribute]: + """Get the optional attributes of this class. + + The optional attributes are the non-mandatory attributes (those not + returned by the `attributes` property) that have the class defined + as their domain, or any of its superclasses. + """ + superclass: OntologyClass + attributes = frozenset( + self._direct_optional_attributes + | { + attribute + for superclass in self.direct_superclasses + for attribute in superclass.optional_attributes + } + ) + return attributes + + @property + def axioms(self) -> FrozenSet[Union[Restriction, Composition]]: """Get all the axioms for the ontology class. - Includes axioms inherited from its superclasses. + Axioms are OWL Restrictions and Compositions. Includes axioms inherited + from its superclasses. Returns: Axioms for the ontology class. @@ -112,37 +133,39 @@ def axioms(self) -> FrozenSet[Restriction]: def __call__( self, session=None, - iri: Optional[Union[URIRef, str, UID]] = None, - uid: Optional[Union[UID, UUID, str, Node, int, bytes]] = None, + iri: Optional[Union[URIRef, str]] = None, + identifier: Optional[Union[UUID, str, Node, int, bytes]] = None, _force: bool = False, **kwargs, ): """Create an OntologyIndividual object from this ontology class. Args: - uid: The identifier of the Cuds object. Should be set to None in - most cases. Then a new identifier is generated, defaults to - None. Defaults to None. - iri: The same as the uid, but exclusively for IRI identifiers. - session (Session, optional): The session to create the cuds object - in, defaults to None. Defaults to None. - _force: Skip validity checks. Defaults to False. + identifier: The identifier of the ontology individual. When set to + a string, has the same effect as the keyword argument `iri`. + When set to`None`, a new identifier with a random UUID is + generated. When set to any of the other accepted types, the + given value is used to generate the UUID of the identifier. + Defaults to None. + iri: The same as the identifier, but exclusively for IRI + identifiers. + session: The session that the ontology individual will be stored + in. Defaults to `None` (the default session). Raises: TypeError: Error occurred during instantiation. Returns: - Cuds, The created cuds object + The new ontology individual. """ - # TODO: Create ontology individuals, NOT CUDS objects. - if None not in (uid, iri): + if None not in (identifier, iri): raise ValueError( "Tried to initialize an ontology individual, both its IRI and " "UID. An ontology individual is constrained to have just one " "UID." ) - elif uid is not None and not isinstance( - uid, (UID, UUID, str, Node, int, bytes) + elif identifier is not None and not isinstance( + identifier, (UID, UUID, str, Node, int, bytes) ): raise ValueError( "Provide an object of one of the following types as UID: " @@ -154,8 +177,8 @@ def __call__( + ",".join(str(x) for x in (URIRef, str, UID)) ) else: - uid = ( - (UID(uid) if uid else None) + identifier = ( + (UID(identifier) if identifier else None) or (UID(iri) if iri else None) or UID() ) @@ -165,7 +188,7 @@ def __call__( # build attributes dictionary by combining # kwargs and defaults return OntologyIndividual( - uid=uid, + uid=identifier, session=session, class_=self, attributes=self._kwargs_to_attributes(kwargs, _skip_checks=_force), @@ -192,26 +215,6 @@ def __init__( """ super().__init__(uid, session, triples, merge=merge) - @property - @lru_cache_timestamp(lambda self: self.session.entity_cache_timestamp) - def optional_attributes(self) -> FrozenSet[OntologyAttribute]: - """Get the optional attributes of this class. - - The optional attributes are the non-mandatory attributes (those not - returned by the `attributes` property) that have the class defined - as their domain, or any of its superclasses. - """ - superclass: OntologyClass - attributes = frozenset( - self._direct_optional_attributes - | { - attribute - for superclass in self.direct_superclasses - for attribute in superclass.optional_attributes - } - ) - return attributes - @lru_cache_timestamp(lambda self: self.session.entity_cache_timestamp) def _compute_axioms( self, identifier: Identifier, predicate: URIRef @@ -228,7 +231,7 @@ def _compute_axioms( Returns: Tuple of computed axioms. """ - axioms: Set[Restriction] = set() + axioms: Set[Union[Restriction, Composition]] = set() for o in self.session.graph.objects(identifier, predicate): if not isinstance(o, BNode): continue diff --git a/simphony_osp/ontology/restriction.py b/simphony_osp/ontology/restriction.py index ac2d51c1..eef9cf37 100644 --- a/simphony_osp/ontology/restriction.py +++ b/simphony_osp/ontology/restriction.py @@ -21,25 +21,25 @@ class QUANTIFIER(Enum): - """The different quantifiers for restrictions.""" + """Quantifiers for restrictions.""" - SOME = 1 - ONLY = 2 - EXACTLY = 3 - MIN = 4 - MAX = 5 - VALUE = 6 + SOME: int = 1 + ONLY: int = 2 + EXACTLY: int = 3 + MIN: int = 4 + MAX: int = 5 + VALUE: int = 6 class RTYPE(Enum): - """The two types of restrictions.""" + """Types of restrictions.""" ATTRIBUTE_RESTRICTION = 1 RELATIONSHIP_RESTRICTION = 2 class Restriction(OntologyEntity): - """A class to represent restrictions on ontology classes.""" + """Restrictions on ontology classes.""" rdf_type = OWL.Restriction rdf_identifier = BNode @@ -70,21 +70,21 @@ def __init__( ) super().__init__(uid, session, triples, merge=merge) - # Public API - # ↓ ------ ↓ - def __str__(self) -> str: """Transform to string.""" return " ".join( map(str, (self._property, self.quantifier, self.target)) ) + # Public API + # ↓ ------ ↓ + @property def quantifier(self) -> QUANTIFIER: """Get the quantifier of the restriction. Returns: - QUANTIFIER: The quantifier of the restriction. + The quantifier of the restriction. """ quantifier, _ = self._get_quantifier_and_target() return quantifier @@ -96,7 +96,7 @@ def target(self) -> Union[OntologyClass, URIRef]: Returns: The target class or datatype. """ - _, target = self._get_quantifier_and_target() + quantifier, target = self._get_quantifier_and_target() try: target = self.session.from_identifier(target) except KeyError: @@ -105,7 +105,7 @@ def target(self) -> Union[OntologyClass, URIRef]: @property def relationship(self) -> OntologyRelationship: - """The relationship the RELATIONSHIP_RESTRICTION acts on. + """The relationship that the RELATIONSHIP_RESTRICTION acts on. Raises: AttributeError: Called on an ATTRIBUTE_RESTRICTION. @@ -119,12 +119,10 @@ def relationship(self) -> OntologyRelationship: @property def attribute(self) -> OntologyAttribute: - """The attribute the restriction acts on. - - Only for ATTRIBUTE_RESTRICTIONs. + """The attribute that the ATTRIBUTE_RESTRICTION acts on. Raises: - AttributeError: self is a RELATIONSHIP_RESTRICTIONs. + AttributeError: Called on a RELATIONSHIP_RESTRICTION. Returns: The attribute. @@ -135,7 +133,7 @@ def attribute(self) -> OntologyAttribute: @property def rtype(self) -> RTYPE: - """Return the type of restriction. + """Type of restriction. Whether the restriction acts on attributes or relationships. diff --git a/simphony_osp/session/session.py b/simphony_osp/session/session.py index c1c8a18b..89ed355b 100644 --- a/simphony_osp/session/session.py +++ b/simphony_osp/session/session.py @@ -203,10 +203,6 @@ class SessionSet(DataStructureSet): This class looks like and acts like the standard `set`, but it is an interface to the methods from `Session` that manage the addition and removal of individuals. - - This class does not hold any information itself, thus it is safe to - spawn multiple instances linked to the same session (when - single-threading). """ _session: Session @@ -443,7 +439,7 @@ def _iter_identifiers(self) -> Iterator[Optional[OntologyIndividual]]: class Session(Environment): - """Interface to a Graph containing OWL ontology entities.""" + """"Box" that stores ontology individuals.""" # ↓ --------------------- Public API --------------------- ↓ # """These methods are meant to be available to the end-user.""" @@ -455,65 +451,6 @@ class Session(Environment): Python (string representation of the session). It has no other effect. """ - @property - def ontology(self) -> Session: - """Another session considered to be the T-Box of this one. - - In a normal setting, a session is considered only to contain an A-Box. - When it is necessary to look for a class, a relationship, an attribute - or an annotation property, the session will look there for their - definition. - """ - return self._ontology or Session.default_ontology - - @ontology.setter - def ontology(self, value: Optional[Session]) -> None: - """Set the T-Box of this session.""" - if not isinstance(value, (Session, type(None))): - raise TypeError( - f"Expected {Session} or {type(None)}, not type {value}." - ) - self._ontology = value - - label_properties: Tuple[URIRef] = (SKOS.prefLabel, RDFS.label) - """The identifiers of the RDF predicates to be considered as labels. - - The entity labels are used, for example, to be able to get ontology - entities from namespace or session objects by such label. - - The order in which the properties are specified in the tuple matters. To - determine the label of an object, the properties will be checked from - left to right, until one of them is defined for that specific entity. - This will be the label of such ontology entity. The rest of the - properties to the right of such property will be ignored for that - entity. - - For example, in the default case above, if an entity has an - `SKOS.prefLabel` it will be considered to be its label, even if it also - has an `RDFS.label`, which will be ignored. If another entity has no - `SKOS.prefLabel` but has a `RDFS.label`, then the `RDFS.label` will - define its label. This means that for some entity, one label property - may be used while for another, a different property can be in use. If - none of the properties are defined, then the entity is considered to - have no label. - """ - - label_languages: Tuple[URIRef] = ("en",) - # TODO: Set to user's language preference from the OS (users can usually - # set such a list in modern operating systems). - """The preferred languages for the default label. - - Normally, entities will be available from all languages. However, - in some places the label has to be printed. In such cases this default - label will be used. - - When defining the label for an object as described in the - `label_properties` docstring above, this list will also be checked from - left to right. When one of the languages specified is available, - this will define the default label. Then the default label will default to - english. If also not available, then any language will be used. - """ - def commit(self) -> None: """Commit pending changes to the session's graph.""" self._graph.commit() @@ -555,8 +492,8 @@ def close(self) -> None: def sparql(self, query: str, ontology: bool = False) -> QueryResult: """Perform a SPARQL CONSTRUCT, DESCRIBE, SELECT or ASK query. - The query is performed on the session's data (the ontology is not - included). + By default, the query is performed only on the session's data (the + ontology is not included). Args: query: String to use as query. @@ -587,6 +524,10 @@ def __enter__(self): self.creation_set = set() return self + def __exit__(self, exc_type, exc_val, exc_tb): + """Restores the previous default session.""" + return super().__exit__(exc_type, exc_val, exc_tb) + def __contains__(self, item: OntologyEntity): """Check whether an ontology entity is stored on the session.""" return item.session is self @@ -594,7 +535,8 @@ def __contains__(self, item: OntologyEntity): def __iter__(self) -> Iterator[OntologyEntity]: """Iterate over all the ontology entities in the session. - This operation can be computationally VERY expensive. + Be careful when using this operation, as it can be computationally very + expensive. """ # Warning: entities can be repeated. return ( @@ -606,109 +548,6 @@ def __len__(self) -> int: """Return the number of ontology entities within the session.""" return sum(1 for _ in self) - def __str__(self): - """Convert the session to a string.""" - # TODO: Return the kind of RDFLib store attached too. - return ( - f"<{self.__class__.__module__}.{self.__class__.__name__}: " - f"{self.identifier if self.identifier is not None else ''} " - f"at {hex(id(self))}>" - ) - - @lru_cache_weak(maxsize=4096) - # On `__init__.py` there is an option to bypass this cache when the - # session is not a T-Box. - def from_identifier(self, identifier: Node) -> OntologyEntity: - """Get an ontology entity from its identifier. - - Args: - identifier: The identifier of the entity. - - Raises: - KeyError: The ontology entity is not stored in this session. - - Returns: - The OntologyEntity. - """ - # WARNING: This method is a central point in SimPhoNy. Change with - # care. - # TIP: Since the method is a central point in SimPhoNy, any - # optimization it gets will speed up SimPhoNy, while bad code in - # this method will slow it down. - - # Look for embedded classes. - compatible = { - rdf_type: compatible_classes(rdf_type, identifier) - for rdf_type in self._graph.objects(identifier, RDF_type) - } - - # If not an embedded class, then the type may be known in - # the ontology. This means that an ontology individual would - # have to be spawned. - for rdf_type, found in compatible.items(): - if not found: - try: - self.ontology.from_identifier(rdf_type) - found |= {OntologyIndividual} - break - except KeyError: - pass - - compatible = set().union(*compatible.values()) - - if ( - OntologyRelationship not in compatible - and (identifier, OWL_inverseOf, None) in self._graph - ): - compatible |= {OntologyRelationship} - - """Some ontologies are hybrid RDFS and OWL ontologies (i.e. FOAF). - In such cases, object and datatype properties are preferred to - annotation properties.""" - if OntologyAnnotation in compatible and ( - compatible & {OntologyRelationship, OntologyAttribute} - ): - compatible.remove(OntologyAnnotation) - - """Finally return the single compatible class or raise an exception.""" - if len(compatible) >= 2: - raise RuntimeError( - f"Two or more python classes (" - f"{', '.join(map(str, compatible))}) " - f"could be spawned from {identifier}." - ) - try: - python_class = compatible.pop() - return python_class(uid=UID(identifier), session=self, merge=None) - except KeyError: - raise KeyError( - f"Identifier {identifier} does not match any OWL " - f"entity, any entity natively supported by " - f"SimPhoNy, nor an ontology individual " - f"belonging to a class in the ontology." - ) - - def from_identifier_typed( - self, identifier: Node, typing: Type[ENTITY] - ) -> ENTITY: - """Get an ontology entity from its identifier, enforcing a type check. - - Args: - identifier: The identifier of the entity. - typing: The expected type of the ontology entity matching the - given identifier. - - Raises: - KeyError: The ontology entity is not stored in this session. - - Returns: - The OntologyEntity. - """ - entity = self.from_identifier(identifier) - if not isinstance(entity, typing): - raise TypeError(f"{identifier} is not of class {typing}.") - return entity - @lru_cache_weak(maxsize=4096) # On `__init__.py` there is an option to bypass this cache when the # session is not a T-Box. @@ -776,7 +615,31 @@ def add( exists_ok: bool = False, all_triples: bool = False, ) -> Union[OntologyIndividual, FrozenSet[OntologyIndividual]]: - """Copies the ontology entities to the session.""" + """Copies ontology individuals to the session. + + Args: + individuals: Ontology individuals to add to this session. + merge: Whether to merge individuals with existing ones if their + identifiers match (read the SimPhoNy documentation for more + details). + exists_ok: Merge or overwrite individuals when they already exist + in the session rather than raising an exception. + all_triples: + When the individual is attached through an object property + to another one which is not properly defined (i.e. has no type + assigned), such connection is generally dropped. Setting this + option to `True` keeps such connections on the copy. Can give + rise to bugs. A common case in which you might want to do this + involves the `dcat:accessURL` object property. + + Returns: + The new copies of the individuals. + + Raises: + RuntimeError: The individual being added has an identifier that + matches the identifier of an individual that already exists in the + session. + """ # Unpack iterables individuals = list( individual @@ -868,7 +731,16 @@ def delete( Iterable[Union[OntologyEntity, Identifier]], ], ): - """Remove an ontology entity from the session.""" + """Remove ontology individuals from the session. + + Args: + entities: Ontology individuals to remove from the session. It is + also possible to just provide their identifiers. + + Raises: + ValueError: When at least one of the given ontology individuals is + not contained in the session. + """ entities = frozenset( entity for x in entities @@ -889,7 +761,11 @@ def delete( self._graph.remove((None, None, entity)) def clear(self, force: bool = False): - """Clear all the data stored in the session.""" + """Clear all the data stored in the session. + + Args: + force: Try to clear read-only sessions too. + """ graph = self._graph_writable if force else self._graph graph.remove((None, None, None)) self._namespaces.clear() @@ -921,6 +797,10 @@ def get( the call. See the "Returns:" section of this docstring for more details on this. + Note: If you are reading the SimPhoNy documentation API Reference, it + is likely that you cannot read this docstring. As a workaround, click + the `source` button to read it in its raw form. + Args: individuals: Restrict the individuals to be returned to a certain subset of the individuals in the session. @@ -1002,6 +882,10 @@ def iter( the call. See the "Returns:" section of this docstring for more details on this. + Note: If you are reading the SimPhoNy documentation API Reference, it + is likely that you cannot read this docstring. As a workaround, click + the `source` button to read it in its raw form. + Args: individuals: Restrict the individuals to be returned to a certain subset of the individuals in the session. @@ -1094,6 +978,26 @@ def iterator() -> Iterator[Optional[OntologyIndividual]]: new ontology is loaded into the session). """ + @property + def ontology(self) -> Session: + """Another session considered to be the T-Box of this one. + + In a normal setting, a session is considered only to contain an A-Box. + When it is necessary to look for a class, a relationship, an attribute + or an annotation property, the session will look there for their + definition. + """ + return self._ontology or Session.default_ontology + + @ontology.setter + def ontology(self, value: Optional[Session]) -> None: + """Set the T-Box of this session.""" + if not isinstance(value, (Session, type(None))): + raise TypeError( + f"Expected {Session} or {type(None)}, not type {value}." + ) + self._ontology = value + _ontology: Optional[Session] = None """Private pointer to the T-Box of the session. @@ -1102,6 +1006,45 @@ def iterator() -> Iterator[Optional[OntologyIndividual]]: which is by default a session containing all the installed ontologies). """ + label_predicates: Tuple[URIRef] = (SKOS.prefLabel, RDFS.label) + """The identifiers of the RDF predicates to be considered as labels. + + The entity labels are used, for example, to be able to get ontology + entities from namespace or session objects by such label. + + The order in which the properties are specified in the tuple matters. To + determine the label of an object, the properties will be checked from + left to right, until one of them is defined for that specific entity. + This will be the label of such ontology entity. The rest of the + properties to the right of such property will be ignored for that + entity. + + For example, in the default case above, if an entity has an + `SKOS.prefLabel` it will be considered to be its label, even if it also + has an `RDFS.label`, which will be ignored. If another entity has no + `SKOS.prefLabel` but has a `RDFS.label`, then the `RDFS.label` will + define its label. This means that for some entity, one label property + may be used while for another, a different property can be in use. If + none of the properties are defined, then the entity is considered to + have no label. + """ + + label_languages: Tuple[URIRef] = ("en",) + # TODO: Set to user's language preference from the OS (users can usually + # set such a list in modern operating systems). + """The preferred languages for the default label. + + Normally, entities will be available from all languages. However, + in some places the label has to be printed. In such cases this default + label will be used. + + When defining the label for an object as described in the + `label_predicates` docstring above, this list will also be checked from + left to right. When one of the languages specified is available, + this will define the default label. Then the default label will default to + english. If also not available, then any language will be used. + """ + def __init__( self, base: Optional[Graph] = None, # The graph must be OPEN already. @@ -1111,7 +1054,11 @@ def __init__( namespaces: Dict[str, URIRef] = None, from_parser: Optional[OntologyParser] = None, ): - """Initialize the session.""" + """Initializes the session. + + The keyword arguments are used internally by SimPhoNy and are not meant + to be set manually. + """ super().__init__() self._environment_references.add(self) # Base the session graph either on a store if passed or an empty graph. @@ -1194,6 +1141,109 @@ def bypassed(*args, **kwargs): for key, value in namespaces.items(): self.bind(key, value) + def __str__(self): + """Convert the session to a string.""" + # TODO: Return the kind of RDFLib store attached too. + return ( + f"<{self.__class__.__module__}.{self.__class__.__name__}: " + f"{self.identifier if self.identifier is not None else ''} " + f"at {hex(id(self))}>" + ) + + @lru_cache_weak(maxsize=4096) + # On `__init__.py` there is an option to bypass this cache when the + # session is not a T-Box. + def from_identifier(self, identifier: Node) -> OntologyEntity: + """Get an ontology entity from its identifier. + + Args: + identifier: The identifier of the entity. + + Raises: + KeyError: The ontology entity is not stored in this session. + + Returns: + The OntologyEntity. + """ + # WARNING: This method is a central point in SimPhoNy. Change with + # care. + # TIP: Since the method is a central point in SimPhoNy, any + # optimization it gets will speed up SimPhoNy, while bad code in + # this method will slow it down. + + # Look for embedded classes. + compatible = { + rdf_type: compatible_classes(rdf_type, identifier) + for rdf_type in self._graph.objects(identifier, RDF_type) + } + + # If not an embedded class, then the type may be known in + # the ontology. This means that an ontology individual would + # have to be spawned. + for rdf_type, found in compatible.items(): + if not found: + try: + self.ontology.from_identifier(rdf_type) + found |= {OntologyIndividual} + break + except KeyError: + pass + + compatible = set().union(*compatible.values()) + + if ( + OntologyRelationship not in compatible + and (identifier, OWL_inverseOf, None) in self._graph + ): + compatible |= {OntologyRelationship} + + """Some ontologies are hybrid RDFS and OWL ontologies (i.e. FOAF). + In such cases, object and datatype properties are preferred to + annotation properties.""" + if OntologyAnnotation in compatible and ( + compatible & {OntologyRelationship, OntologyAttribute} + ): + compatible.remove(OntologyAnnotation) + + """Finally return the single compatible class or raise an exception.""" + if len(compatible) >= 2: + raise RuntimeError( + f"Two or more python classes (" + f"{', '.join(map(str, compatible))}) " + f"could be spawned from {identifier}." + ) + try: + python_class = compatible.pop() + return python_class(uid=UID(identifier), session=self, merge=None) + except KeyError: + raise KeyError( + f"Identifier {identifier} does not match any OWL " + f"entity, any entity natively supported by " + f"SimPhoNy, nor an ontology individual " + f"belonging to a class in the ontology." + ) + + def from_identifier_typed( + self, identifier: Node, typing: Type[ENTITY] + ) -> ENTITY: + """Get an ontology entity from its identifier, enforcing a type check. + + Args: + identifier: The identifier of the entity. + typing: The expected type of the ontology entity matching the + given identifier. + + Raises: + KeyError: The ontology entity is not stored in this session. + + Returns: + The OntologyEntity. + """ + entity = self.from_identifier(identifier) + if not isinstance(entity, typing): + raise TypeError(f"{identifier} is not of class {typing}.") + return entity + def merge(self, entity: OntologyEntity) -> None: """Merge a given ontology entity with what is in the session. @@ -1438,7 +1488,7 @@ def filter_language(literal): labels = ( (prop, literal, subject) - for prop in self.label_properties + for prop in self.label_predicates for subject, _, literal in self._graph.triples( (entity, prop, None) ) diff --git a/simphony_osp/tools/general.py b/simphony_osp/tools/general.py index 51442ac2..726a2c24 100644 --- a/simphony_osp/tools/general.py +++ b/simphony_osp/tools/general.py @@ -42,14 +42,15 @@ def branch( def relationships_between( subj: OntologyIndividual, obj: OntologyIndividual ) -> Set[OntologyRelationship]: - """Get the set of relationships between two cuds objects. + """Get the set of relationships between two ontology individuals. Args: - subj: The subject ontology individual. - obj: The object ontology individual. + subj: The subject of the relationship. + obj: The object (target) of the relationship. Returns: - The set of relationships between subject and object. + The set of relationships between the given subject and object + individuals. """ return { relationship diff --git a/simphony_osp/tools/import_export.py b/simphony_osp/tools/import_export.py index 67cd92bf..913f836f 100644 --- a/simphony_osp/tools/import_export.py +++ b/simphony_osp/tools/import_export.py @@ -29,6 +29,10 @@ def import_file( ) -> Union[OntologyIndividual, Set[OntologyIndividual]]: """Imports ontology individuals from a file and load them into a session. + Note: If you are reading the SimPhoNy documentation API Reference, it + is likely that you cannot read this docstring. As a workaround, click + the `source` button to read it in its raw form. + Args: path_or_filelike: either, (str) the path of a file to import; @@ -171,6 +175,10 @@ def export_file( ) -> Union[str, None]: """Exports ontology individuals to a variety of formats. + Note: If you are reading the SimPhoNy documentation API Reference, it + is likely that you cannot read this docstring. As a workaround, click + the `source` button to read it in its raw form. + Args: individuals_or_session: (OntologyIndividual) A single ontology individual to export, or diff --git a/simphony_osp/tools/pico.py b/simphony_osp/tools/pico.py index 7d6d1de2..8a27c92b 100644 --- a/simphony_osp/tools/pico.py +++ b/simphony_osp/tools/pico.py @@ -1,4 +1,4 @@ -"""Pico is a commandline tool used to install ontologies.""" +"""Pico is a tool used to install ontologies.""" import argparse import logging @@ -25,9 +25,8 @@ def install(*files: Union[Path, str], overwrite: bool = False) -> None: """Install ontology packages. Args: - files: Paths of `yml` files describing the ontologies to install. - Alternatively, identifiers of ontology packages that are - bundled with SimPhoNy. + files: Paths of ontology packages to install. Alternatively, + identifiers of ontology packages that are bundled with SimPhoNy. overwrite: Whether to overwrite already installed ontology packages. """ diff --git a/simphony_osp/tools/pretty_print.py b/simphony_osp/tools/pretty_print.py index 59fd682b..2cb659a1 100644 --- a/simphony_osp/tools/pretty_print.py +++ b/simphony_osp/tools/pretty_print.py @@ -22,14 +22,18 @@ def pretty_print( ] = owl.topObjectProperty, file=sys.stdout, ): - """Print the given ontology entity in a human-readable way. + """Print a tree-like, text representation stemming from an individual. - The identifier, the type, the ancestors and the content are printed. + Generates a tree-like, text-based representation stemming from a given + ontology individual, that includes the IRI, ontology classes and attributes + of the involved individuals, as well as the relationships connecting them. Args: - entity: Entity to be printed. - file: The file to print to. - rel: The relationships to consider when searching for sub-elements. + entity: Ontology individual to be used as starting point of the + text-based representation. + file: A file to print the text to. Defaults to the standard output. + rel: Restrict the relationships to consider when searching for + attached individuals to subclasses of the given relationships. """ # Fix the order of each element by pre-populating a dictionary. pp = { diff --git a/simphony_osp/tools/search.py b/simphony_osp/tools/search.py index e909d0db..8f099bf3 100644 --- a/simphony_osp/tools/search.py +++ b/simphony_osp/tools/search.py @@ -48,7 +48,7 @@ def find( float("inf") (unlimited). Returns: - The element(s) found. One element (or `None` is returned when + The element(s) found. One element (or `None`) is returned when `find_all` is `False`, a generator when `find_all` is True. """ if isinstance(rel, (OntologyRelationship, Node)): @@ -202,10 +202,11 @@ def find_relationships( Iterable[Union[OntologyRelationship, Node]], ] = OWL.topObjectProperty, ) -> Iterator[OntologyIndividual]: - """Find the given relationship in the subtree of the given root. + """Find given relationship in the subgraph reachable from the given root. Args: - root: Only consider the subgraph rooted in this root. + root: Only consider the subgraph of individuals reachable from this + root. find_rel: The relationship to find. find_sub_relationships: Treat relationships that are a sub-relationship of the relationship to find as valid results. diff --git a/simphony_osp/tools/semantic2dot.py b/simphony_osp/tools/semantic2dot.py index eb1f105a..3066e701 100644 --- a/simphony_osp/tools/semantic2dot.py +++ b/simphony_osp/tools/semantic2dot.py @@ -25,7 +25,12 @@ class Semantic2Dot: - """Utility for creating a dot and png representation of semantic data.""" + """Class for ojects returned by the `semantic2dot` plotting tool. + + Objects of this class produced as outcome of calling the `semantic2dot` + plotting tool. They hold the graph information and can be used either to + display it in a Jupyter notebook or render the graph to a file. + """ def __init__( self, @@ -526,11 +531,15 @@ def semantic2dot( Union[OntologyRelationship, Iterable[OntologyRelationship]] ] = None, ) -> Semantic2Dot: - """Utility for plotting ontologies. + """Utility for plotting ontology entities. + + Note: If you are reading the SimPhoNy documentation API Reference, it + is likely that you cannot read this docstring. As a workaround, click + the `source` button to read it in its raw form. - Plot A-Boxes (ontology individuals) and the relationships between them, - plot T-Boxes (classes, relationships and attributes), or a combination - of them. + Plot assertional knowledge (ontology individuals and the relationships + between them), plot terminological knowledge + (classes, relationships and attributes), or a combination of them. Args: elements: Elements to plot: @@ -542,7 +551,7 @@ def semantic2dot( multiple are provided; rel: When not `None` and when plotting an ontology individual, calls uses the method `find(individual, rel=rel, find_all=True)` from - `simphony_osp.tools` to additionally plot such individuals. + `simphony_osp.tools.search` to additionally plot such individuals. """ return Semantic2Dot(*elements, rel=rel) diff --git a/tests/test_api.py b/tests/test_api.py index 016fa2bd..9d9a2262 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -82,7 +82,7 @@ def test_ontology(self): self.assertIs(abox.ontology, ontology) def test_label_properties(self): - """Test the label_properties attribute of a session. + """Test the label_predicates attribute of a session. The test also changes the properties and verifies that the session reacts as expected. @@ -90,9 +90,9 @@ def test_label_properties(self): from simphony_osp.namespaces import city with Session() as session: - self.assertIsInstance(session.label_properties, tuple) + self.assertIsInstance(session.label_predicates, tuple) self.assertTrue( - all(isinstance(x, URIRef) for x in session.label_properties) + all(isinstance(x, URIRef) for x in session.label_predicates) ) fr = city.City(name="Freiburg", coordinates=[0, 0]) @@ -115,7 +115,7 @@ def test_label_properties(self): self.assertEqual(fr.label, "Freiburg prefLabel") - session.label_properties = (RDFS.label, SKOS.prefLabel) + session.label_predicates = (RDFS.label, SKOS.prefLabel) self.assertEqual(fr.label, "Freiburg label") def test_label_languages(self): From b7eb8a551d8a0077643f0e0ef803a90e65fecabd Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Fri, 9 Sep 2022 16:21:43 +0000 Subject: [PATCH 2/7] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- simphony_osp/session/session.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/simphony_osp/session/session.py b/simphony_osp/session/session.py index 89ed355b..6ae7d979 100644 --- a/simphony_osp/session/session.py +++ b/simphony_osp/session/session.py @@ -439,7 +439,7 @@ def _iter_identifiers(self) -> Iterator[Optional[OntologyIndividual]]: class Session(Environment): - """"Box" that stores ontology individuals.""" + """ "Box" that stores ontology individuals.""" # ↓ --------------------- Public API --------------------- ↓ # """These methods are meant to be available to the end-user.""" From 1f1b682226804f701c51d0357d5329ff05c9fc2d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Manuel=20Dom=C3=ADnguez?= Date: Fri, 9 Sep 2022 18:56:49 +0200 Subject: [PATCH 3/7] Use FOAF from Web Archive to pass tests (FOAF site is down) --- simphony_osp/ontology/files/foaf.yml | 2 +- tests/test_api.py | 8 ++++++-- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/simphony_osp/ontology/files/foaf.yml b/simphony_osp/ontology/files/foaf.yml index 344f8086..18971f65 100644 --- a/simphony_osp/ontology/files/foaf.yml +++ b/simphony_osp/ontology/files/foaf.yml @@ -1,4 +1,4 @@ identifier: foaf -ontology_file: "https://xmlns.com/foaf/spec/index.rdf" +ontology_file: "https://web.archive.org/web/20220614185720if_/http://xmlns.com/foaf/spec/index.rdf" namespaces: foaf: "http://xmlns.com/foaf/0.1/" diff --git a/tests/test_api.py b/tests/test_api.py index 9d9a2262..93b272ab 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -1786,12 +1786,16 @@ def setUpClass(cls): with tempfile.NamedTemporaryFile( "w", suffix=".yml", delete=False ) as file: - foaf_modified: str = """ + foaf_url = ( + "https://web.archive.org/web/20220614185720if_/" + "http://xmlns.com/foaf/spec/index.rdf" + ) + foaf_modified: str = f""" identifier: foaf format: xml namespaces: foaf: http://xmlns.com/foaf/0.1/ - ontology_file: https://xmlns.com/foaf/spec/index.rdf + ontology_file: {foaf_url} """ file.write(foaf_modified) file.seek(0) From a713430918ce1b91000981a262e6562300401224 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Manuel=20Dom=C3=ADnguez?= Date: Fri, 9 Sep 2022 19:10:47 +0200 Subject: [PATCH 4/7] Fix API test --- tests/test_api.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/test_api.py b/tests/test_api.py index 93b272ab..2402d525 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -2539,12 +2539,12 @@ def test_application_json_doc_city(self): json_doc = json.loads(file.read()) with Session() as session: c = branch( - city.City(name="Freiburg", coordinates=[0, 0], uid=1), + city.City(name="Freiburg", coordinates=[0, 0], identifier=1), city.Neighborhood( - name="Littenweiler", coordinates=[0, 0], uid=2 + name="Littenweiler", coordinates=[0, 0], identifier=2 ), city.Street( - name="Schwarzwaldstraße", coordinates=[0, 0], uid=3 + name="Schwarzwaldstraße", coordinates=[0, 0], identifier=3 ), rel=city.hasPart, ) From 178511e1042aa40f66f052b387ee39b2c4a41def Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Manuel=20Dom=C3=ADnguez?= Date: Fri, 9 Sep 2022 19:10:55 +0200 Subject: [PATCH 5/7] Fix bug with SQLite wrapper --- simphony_osp/interfaces/sqlite/interface.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/simphony_osp/interfaces/sqlite/interface.py b/simphony_osp/interfaces/sqlite/interface.py index e18e46ac..54e09d8e 100644 --- a/simphony_osp/interfaces/sqlite/interface.py +++ b/simphony_osp/interfaces/sqlite/interface.py @@ -27,6 +27,7 @@ def open(self, configuration: str, create: bool = False): raise FileNotFoundError( f"Database file {configuration} does not " f"exist." ) - return super().open(configuration="sqlite:///" + configuration) + return super().open(configuration="sqlite:///" + configuration, + create=create) # ↑ ---------------- ↑ From b2ebaea75e283b85faa209d5f2b988f340f129a1 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Fri, 9 Sep 2022 17:12:30 +0000 Subject: [PATCH 6/7] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- simphony_osp/interfaces/sqlite/interface.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/simphony_osp/interfaces/sqlite/interface.py b/simphony_osp/interfaces/sqlite/interface.py index 54e09d8e..74a87717 100644 --- a/simphony_osp/interfaces/sqlite/interface.py +++ b/simphony_osp/interfaces/sqlite/interface.py @@ -27,7 +27,8 @@ def open(self, configuration: str, create: bool = False): raise FileNotFoundError( f"Database file {configuration} does not " f"exist." ) - return super().open(configuration="sqlite:///" + configuration, - create=create) + return super().open( + configuration="sqlite:///" + configuration, create=create + ) # ↑ ---------------- ↑ From 357c342a621df5a6777474296b405cc15269d8da Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Manuel=20Dom=C3=ADnguez?= Date: Fri, 9 Sep 2022 19:29:31 +0200 Subject: [PATCH 7/7] Fix flake8 --- simphony_osp/ontology/oclass.py | 1 + simphony_osp/session/session.py | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/simphony_osp/ontology/oclass.py b/simphony_osp/ontology/oclass.py index 663b0ea5..877ee313 100644 --- a/simphony_osp/ontology/oclass.py +++ b/simphony_osp/ontology/oclass.py @@ -22,6 +22,7 @@ from rdflib.term import Identifier, Node from simphony_osp.ontology.attribute import OntologyAttribute +from simphony_osp.ontology.composition import Composition from simphony_osp.ontology.entity import OntologyEntity from simphony_osp.ontology.restriction import Restriction from simphony_osp.utils.cache import lru_cache_timestamp diff --git a/simphony_osp/session/session.py b/simphony_osp/session/session.py index 6ae7d979..efee0a99 100644 --- a/simphony_osp/session/session.py +++ b/simphony_osp/session/session.py @@ -439,7 +439,7 @@ def _iter_identifiers(self) -> Iterator[Optional[OntologyIndividual]]: class Session(Environment): - """ "Box" that stores ontology individuals.""" + """'Box' that stores ontology individuals.""" # ↓ --------------------- Public API --------------------- ↓ # """These methods are meant to be available to the end-user."""