import datetime
import operator
import typing
from typing import Dict, Iterable, FrozenSet, Optional, Set, Tuple, Iterator
from .definition import Definition
from .entity import Entity, EntityData, EntitySet
from .logic import SubpropertiesIterator, SuperpropertiesIterator
from .logic.lineage import SubpropertiesHandler, SuperpropertiesHandler
from .pv import PropertyValue
from .synonym import SynonymData
from .utils.meta import typechecked
from .xref import Xref
if typing.TYPE_CHECKING:
from .ontology import Ontology
from .term import Term
__all__ = ["Relationship", "RelationshipData", "RelationshipSet"]
[docs]
class RelationshipData(EntityData):
"""Internal data storage of `Relationship` information."""
id: str
anonymous: bool
name: Optional[str]
namespace: Optional[str]
alternate_ids: Set[str]
definition: Optional[Definition]
comment: Optional[str]
subsets: Set[str]
synonyms: Set[SynonymData]
xrefs: Set[Xref]
annotations: Set[PropertyValue]
domain: Optional[str]
range: Optional[str]
builtin: bool
holds_over_chain: Set[Tuple[str, str]]
antisymmetric: bool
cyclic: bool
reflexive: bool
asymmetric: bool
symmetric: bool
transitive: bool
functional: bool
inverse_functional: bool
intersection_of: Set[str]
inverse_of: Optional[str]
transitive_over: Set[str]
equivalent_to_chain: Set[Tuple[str, str]]
disjoint_over: Set[str]
obsolete: bool
created_by: Optional[str]
creation_date: Optional[datetime.datetime]
expand_assertion_to: Set[Definition]
expand_expression_to: Set[Definition]
metadata_tag: bool
class_level: bool
if typing.TYPE_CHECKING:
__annotations__: Dict[str, str]
__slots__ = tuple(__annotations__) # noqa: E0602
def __init__(
self,
id: str,
anonymous: bool = False,
name: Optional[str] = None,
namespace: Optional[str] = None,
alternate_ids: Optional[Set[str]] = None,
definition: Optional[Definition] = None,
comment: Optional[str] = None,
subsets: Optional[Set[str]] = None,
synonyms: Optional[Set[SynonymData]] = None,
xrefs: Optional[Set[Xref]] = None,
annotations: Optional[Set[PropertyValue]] = None,
domain: Optional[str] = None,
range: Optional[str] = None,
builtin: bool = False,
holds_over_chain: Optional[Set[Tuple[str, str]]] = None,
antisymmetric: bool = False,
cyclic: bool = False,
reflexive: bool = False,
asymmetric: bool = False,
symmetric: bool = False,
transitive: bool = False,
functional: bool = False,
inverse_functional: bool = False,
intersection_of: Optional[Set[str]] = None,
union_of: Optional[Set[str]] = None,
equivalent_to: Optional[Set[str]] = None,
disjoint_from: Optional[Set[str]] = None,
inverse_of: Optional[str] = None,
transitive_over: Optional[Set[str]] = None,
equivalent_to_chain: Optional[Set[Tuple[str, str]]] = None,
disjoint_over: Optional[Set[str]] = None,
relationships: Optional[Dict[str, Set[str]]] = None,
obsolete: bool = False,
created_by: Optional[str] = None,
creation_date: Optional[datetime.datetime] = None,
replaced_by: Optional[Set[str]] = None,
consider: Optional[Set[str]] = None,
expand_assertion_to: Optional[Set[Definition]] = None,
expand_expression_to: Optional[Set[Definition]] = None,
metadata_tag: bool = False,
class_level: bool = False,
):
self.id = id
self.anonymous = anonymous
self.name = name
self.namespace = namespace
self.alternate_ids = alternate_ids or set()
self.definition = definition
self.comment = comment
self.subsets = subsets or set()
self.synonyms = synonyms or set()
self.xrefs = xrefs or set()
self.annotations = annotations or set()
self.domain = domain
self.range = range
self.builtin = builtin
self.holds_over_chain = holds_over_chain or set()
self.antisymmetric = antisymmetric
self.cyclic = cyclic
self.reflexive = reflexive
self.asymmetric = asymmetric
self.symmetric = symmetric
self.transitive = transitive
self.functional = functional
self.inverse_functional = inverse_functional
self.intersection_of = intersection_of or set()
self.union_of = union_of or set()
self.equivalent_to = equivalent_to or set()
self.disjoint_from = disjoint_from or set()
self.inverse_of = inverse_of
self.transitive_over = transitive_over or set()
self.equivalent_to_chain = equivalent_to_chain or set()
self.disjoint_over = disjoint_over or set()
self.relationships = relationships or dict()
self.obsolete = obsolete
self.created_by = created_by
self.creation_date = creation_date
self.replaced_by = replaced_by or set()
self.consider = consider or set()
self.expand_assertion_to = expand_assertion_to or set()
self.expand_expression_to = expand_expression_to or set()
self.metadata_tag = metadata_tag
self.class_level = class_level
class RelationshipSet(EntitySet["Relationship"]):
"""A specialized mutable set to store `Relationship` instances."""
# --- Magic methods ------------------------------------------------------
def __iter__(self) -> Iterator["Relationship"]:
return map(lambda t: self._ontology.get_relationship(t), iter(self._ids)) # type: ignore
# --- Methods ---------------------------------------------------------
def subproperties(
self, distance: Optional[int] = None, with_self: bool = True
) -> SubpropertiesIterator:
"""Get an iterator over the subproperties of all relationships in the set."""
return SubpropertiesIterator(*self, distance=distance, with_self=with_self)
def superproperties(
self, distance: Optional[int] = None, with_self: bool = True
) -> SuperpropertiesIterator:
"""Get an iterator over the superproperties of all relationships in the set.
Example:
>>> pato = pronto.Ontology("pato.obo")
>>> proportionality_to = pato["PATO:0001470"]
>>> quality_mapping = pronto.RelationshipSet(
... r for r in pato.relationships()
... if r.domain == proportionality_to
... )
>>> sorted(quality_mapping.subproperties().to_set().ids)
['has_dividend_entity', 'has_dividend_quality', ...
"""
return SuperpropertiesIterator(*self, distance=distance, with_self=with_self)
[docs]
class Relationship(Entity["RelationshipData", "RelationshipSet"]):
"""A relationship, constitute the edges of the ontology graph.
Also sometimes refered as typedefs, relationship types, properties or
predicates. Formally equivalent to a property (either ``ObjectProperty``
or ``AnnotationProperty``) in OWL2.
"""
if typing.TYPE_CHECKING:
def __init__(self, ontology: "Ontology", reldata: "RelationshipData"):
super().__init__(ontology, reldata)
def _data(self) -> "RelationshipData":
return typing.cast("RelationshipData", super()._data())
# --- Associated type variables ------------------------------------------
_Set = RelationshipSet
_data_getter = operator.attrgetter("_relationships")
# --- Methods ------------------------------------------------------------
[docs]
def subproperties(
self, distance: Optional[int] = None, with_self: bool = True
) -> "SubpropertiesHandler":
"""Get an handle over the subproperties of this `Relationship`.
Arguments:
distance (int, optional): The maximum distance between this
relationship and the yielded subproperties (`0` for the
relationship itself, `1` for its immediate children, etc.).
Use `None` to explore the entire directed graph transitively.
with_self (bool): Whether or not to include the current term in
the terms being yielded. RDF semantics state that the
``rdfs:subClassOf`` property is reflexive (and therefore is
``rdfs:subPropertyOf`` reflexive too by transitivity), so this
is enabled by default, but in most practical cases only the
distinct subproperties are desired.
"""
return SubpropertiesHandler(self, distance=distance, with_self=with_self)
[docs]
def superproperties(
self, distance: Optional[int] = None, with_self: bool = True
) -> "SuperpropertiesHandler":
"""Get an handle over the superproperties of this `Relationship`.
In order to follow the semantics of ``rdf:subPropertyOf``, which in
turn respects the mathematical definition of subset inclusion, ``is_a``
is defined as a transitive relationship, hence the inverse relationship
is also transitive by closure property.
Arguments:
distance (int, optional): The maximum distance between this
relationship and the yielded subperoperties (`0` for the
relationship itself, `1` for its immediate parents, etc.).
Use `None` to explore the entire directed graph transitively.
with_self (bool): Whether or not to include the current term in
the terms being yielded. RDF semantics state that the
``rdfs:subClassOf`` property is transitive (and therefore is
``rdfs:subPropertyOf`` transitive too), so this is enabled
by default, but in most practical cases only the distinct
subproperties are desired.
"""
return SuperpropertiesHandler(self, distance=distance, with_self=with_self)
# --- Attributes ---------------------------------------------------------
@property
def antisymmetric(self) -> bool:
"""`bool`: Whether this relationship is anti-symmetric."""
return self._data().antisymmetric
@antisymmetric.setter # type: ignore
@typechecked(property=True)
def antisymmetric(self, value: bool) -> None:
self._data().antisymmetric = value
@property
def asymmetric(self) -> bool:
"""`bool`: Whether this relationship is asymmetric."""
return self._data().asymmetric
@asymmetric.setter # type: ignore
@typechecked(property=True)
def asymmetric(self, value: bool) -> None:
self._data().asymmetric = value
@property
def class_level(self) -> bool:
"""`bool`: Whether this relationship is applied at class level.
This tag affects how OBO ``relationship`` tags should be translated
in OWL2: by default, all relationship tags are taken to mean an
all-some relation over an instance level relation. With this flag
set to `True`, the relationship will be translated to an `owl:hasValue`
restriction.
"""
return self._data().class_level
@class_level.setter # type: ignore
@typechecked(property=True)
def class_level(self, value: bool) -> None:
self._data().class_level = value
@property
def cyclic(self) -> bool:
"""`bool`: Whether this relationship is cyclic."""
return self._data().cyclic
@cyclic.setter # type: ignore
@typechecked(property=True)
def cyclic(self, value: bool) -> None:
self._data().cyclic = value
@property
def disjoint_over(self) -> "RelationshipSet":
"""`frozenset`: The relationships this relationships is disjoint over."""
s = RelationshipSet()
s._ids = self._data().disjoint_over
s._ontology = self._ontology()
return s
@property
def domain(self) -> Optional["Term"]:
"""`Term` or `None`: The domain of the relationship, if any."""
data, ontology = self._data(), self._ontology()
if data.domain is not None:
return ontology.get_term(data.domain)
return None
@domain.setter
def domain(self, value: Optional["Term"]) -> None:
rshipdata, ontology = self._data(), self._ontology()
if value is not None:
try:
ontology.get_term(value.id)
except KeyError:
raise ValueError(f"{value} is not a term in {ontology}")
rshipdata.domain = value.id if value is not None else None
@property
def equivalent_to_chain(self) -> FrozenSet[Tuple["Relationship", "Relationship"]]:
return frozenset(
{
tuple(map(self._ontology().get_relationship, chain))
for chain in self._data().equivalent_to_chain
}
)
@equivalent_to_chain.setter
def equivalent_to_chain(self, equivalent_to_chain: Iterable[Tuple["Relationship", "Relationship"]]):
data = self._data()
data.equivalent_to_chain = {
(r1.id, r2.id)
for r1, r2 in equivalent_to_chain
}
@property
def expand_assertion_to(self) -> FrozenSet[Definition]:
return frozenset(self._data().expand_assertion_to)
@property
def expand_expression_to(self) -> FrozenSet[Definition]:
return frozenset(self._data().expand_expression_to)
@property
def functional(self) -> bool:
"""`bool`: Whether this relationship is functional."""
return self._data().functional
@functional.setter # type: ignore
@typechecked(property=True)
def functional(self, value: bool) -> None:
self._data().functional = value
@property
def inverse_functional(self) -> bool:
"""`bool`: Whether this relationship is inverse functional."""
return self._data().inverse_functional
@inverse_functional.setter # type: ignore
@typechecked(property=True)
def inverse_functional(self, value: bool) -> None:
self._data().inverse_functional = value
@property
def metadata_tag(self) -> bool:
"""`bool`: Whether or not this relationship is a metadata tag.
This tag affects how OBO typedefs should be translated in OWL2: by
default, all typedef tags are translated to an `owl:ObjectProperty`.
With this flag set to `True`, the typedef will be translated to an
`owl:AnnotationProperty`.
"""
return self._data().metadata_tag
@metadata_tag.setter # type: ignore
@typechecked(property=True)
def metadata_tag(self, value: bool):
self._data().metadata_tag = value
@property
def holds_over_chain(self) -> FrozenSet[Tuple["Relationship", "Relationship"]]:
"""`frozenset` of `Relationship` couples: The chains this relationship holds over."""
ont: "Ontology" = self._ontology()
data: "RelationshipData" = self._data()
return frozenset(
tuple(map(ont.get_term, chain))
for chain in data.holds_over_chain
)
@holds_over_chain.setter
def holds_over_chain(self, holds_over_chain: Iterable[Tuple["Relationship", "Relationship"]]) -> None:
data: "RelationshipData" = self._data()
data.holds_over_chain = {
(r1.id, r2.id)
for r1, r2 in holds_over_chain
}
@property
def inverse_of(self) -> Optional["Relationship"]:
"""`Relationship` or `None`: The inverse of this relationship, if any."""
ont, reldata = self._ontology(), self._data()
if reldata.inverse_of is not None:
return ont.get_relationship(reldata.inverse_of)
return None
@inverse_of.setter
def inverse_of(self, value: Optional["Relationship"]):
self._data().inverse_of = None if value is None else value.id
@property
def intersection_of(self) -> "RelationshipSet":
"""`RelationshipSet`: The relations this relationship is an intersection of."""
s = RelationshipSet()
s._ids = self._data().intersection_of
s._ontology = self._ontology()
return s
@property
def range(self) -> Optional["Term"]:
"""`Term` or `None`: The range of the relationship, if any."""
range, ont = self._data().range, self._ontology()
return ont.get_term(range) if range is not None else None
@range.setter
def range(self, value: Optional["Term"]):
if value is not None:
try:
self._ontology().get_term(value.id)
except KeyError:
raise ValueError(f"{value} is not in {self._ontology()}")
self._data().range = value.id if value is not None else None
@property
def reflexive(self) -> bool:
"""`bool`: Whether or not the relationship is reflexive."""
return self._data().reflexive
@reflexive.setter # type: ignore
@typechecked(property=True)
def reflexive(self, value: bool):
self._data().reflexive = value
@property
def symmetric(self) -> bool:
"""`bool`: Whether or not the relationship is symmetric."""
return self._data().symmetric
@symmetric.setter # type: ignore
@typechecked(property=True)
def symmetric(self, value: bool):
self._data().symmetric = value
@property
def transitive(self) -> bool:
"""`bool`: Whether or not the relationship is transitive."""
return self._data().transitive
@transitive.setter # type: ignore
@typechecked(property=True)
def transitive(self, value: bool):
self._data().transitive = value
@property
def transitive_over(self) -> "RelationshipSet":
"""`RelationshipSet`: The relations this relationship is transitive over."""
s = RelationshipSet()
s._ids = self._data().transitive_over
s._ontology = self._ontology()
return s
# TODO: remove in v3.0.0
_BUILTINS = {
"is_a": RelationshipData(
id="is_a",
anonymous=False,
name="is a",
namespace=None,
alternate_ids=None,
definition=Definition(
"A subclassing relationship between one term and another",
xrefs=set(
{
Xref(
"http://owlcollab.github.io/oboformat/doc/GO.format.obo-1_4.html"
)
}
),
),
comment=None,
subsets=None,
synonyms=None,
xrefs=None,
annotations=None,
domain=None,
range=None,
builtin=True,
holds_over_chain=None,
antisymmetric=True,
cyclic=True,
reflexive=True,
asymmetric=False,
symmetric=False,
transitive=True,
functional=False,
inverse_functional=False,
intersection_of=None,
union_of=None,
equivalent_to=None,
disjoint_from=None,
inverse_of=None,
transitive_over=None,
equivalent_to_chain=None,
disjoint_over=None,
relationships=None,
obsolete=False,
created_by=None,
creation_date=None,
replaced_by=None,
consider=None,
expand_assertion_to=None, # TODO
expand_expression_to=None, # TODO
metadata_tag=False,
class_level=True,
)
}