# coding: utf-8
import functools
import typing
import weakref
from typing import Iterable, Optional, Set
from .utils.meta import roundrepr, typechecked
from .xref import Xref
if typing.TYPE_CHECKING:
from .ontology import Ontology
_SCOPES = frozenset({"EXACT", "RELATED", "BROAD", "NARROW", None})
__all__ = ["SynonymType", "SynonymData", "Synonym"]
[docs]
@roundrepr
@functools.total_ordering
class SynonymType(object):
"""A user-defined synonym type."""
id: str
description: str
scope: Optional[str]
__slots__ = ("__weakref__", "id", "description", "scope")
@typechecked()
def __init__(self, id: str, description: str, scope: Optional[str] = None):
if scope not in _SCOPES:
raise ValueError(f"invalid synonym scope: {scope}")
self.id = id
self.description = description
self.scope = scope
[docs]
def __eq__(self, other):
if isinstance(other, SynonymType):
return self.id == other.id
return False
[docs]
def __lt__(self, other):
if isinstance(other, SynonymType):
if self.id < other.id:
return True
return self.id == other.id and self.description < other.description
return NotImplemented
[docs]
def __hash__(self):
return hash((SynonymType, self.id))
[docs]
@roundrepr
@functools.total_ordering
class SynonymData(object):
"""Internal data storage of `Synonym` information."""
description: str
scope: Optional[str]
type: Optional[str]
xrefs: Set[Xref]
__slots__ = ("__weakref__", "description", "type", "xrefs", "scope")
[docs]
def __eq__(self, other):
if isinstance(other, SynonymData):
return self.description == other.description and self.scope == other.scope
return False
[docs]
def __lt__(self, other): # FIXME?
if not isinstance(other, SynonymData):
return NotImplemented
if self.type is not None and other.type is not None:
return (self.description, self.scope, self.type, frozenset(self.xrefs)) < (
self.description,
self.scope,
other.type,
frozenset(other.xrefs),
)
else:
return (self.description, self.scope, frozenset(self.xrefs)) < (
self.description,
self.scope,
frozenset(other.xrefs),
)
[docs]
def __hash__(self):
return hash((self.description, self.scope))
def __init__(
self,
description: str,
scope: Optional[str] = None,
type: Optional[str] = None,
xrefs: Optional[Iterable[Xref]] = None,
):
if scope not in _SCOPES:
raise ValueError(f"invalid synonym scope: {scope}")
self.description = description
self.scope = scope
self.type = type
self.xrefs = set(xrefs) if xrefs is not None else set()
[docs]
@functools.total_ordering
class Synonym(object):
"""A synonym for an entity, with respect to the OBO terminology."""
__ontology: "Ontology"
if typing.TYPE_CHECKING:
__data: "weakref.ReferenceType[SynonymData]"
def __init__(self, ontology: "Ontology", data: "SynonymData"):
self.__data = weakref.ref(data)
self.__ontology = ontology
def _data(self) -> SynonymData:
rdata = self.__data()
if rdata is None:
raise RuntimeError("synonym data was deallocated")
return rdata
else:
__slots__: Iterable[str] = ("__weakref__", "__ontology", "_data")
def __init__(self, ontology: "Ontology", syndata: "SynonymData"):
if syndata.type is not None:
if not any(t.id == syndata.type for t in ontology.synonym_types()):
raise ValueError(f"undeclared synonym type: {syndata.type}")
self._data = weakref.ref(syndata)
self.__ontology = ontology
[docs]
def __eq__(self, other: object):
if isinstance(other, Synonym):
return self._data() == other._data()
return False
[docs]
def __lt__(self, other: object):
if not isinstance(other, Synonym):
return False
return self._data().__lt__(other._data())
[docs]
def __hash__(self):
return hash(self._data())
[docs]
def __repr__(self):
return roundrepr.make(
"Synonym",
self.description,
scope=(self.scope, None),
type=(self.type, None),
xrefs=(self.xrefs, set()),
)
@property
def description(self) -> str:
return self._data().description
@description.setter # type: ignore
@typechecked(property=True)
def description(self, description: str) -> None:
self._data().description = description
@property
def type(self) -> Optional[SynonymType]:
ontology, syndata = self.__ontology, self._data()
if syndata.type is not None:
return next(t for t in ontology.synonym_types() if t.id == syndata.type)
return None
@type.setter # type: ignore
@typechecked(property=True)
def type(self, type_: Optional[SynonymType]) -> None:
synonyms: Iterable[SynonymType] = self.__ontology.synonym_types()
if type_ is not None and not any(type_.id == s.id for s in synonyms):
raise ValueError(f"undeclared synonym type: {type_.id}")
self._data().type = type_.id if type_ is not None else None
@property
def scope(self) -> Optional[str]:
return self._data().scope
@scope.setter # type: ignore
@typechecked(property=True)
def scope(self, scope: Optional[str]):
if scope not in _SCOPES:
raise ValueError(f"invalid synonym scope: {scope}")
self._data().scope = scope
@property
def xrefs(self) -> Set[Xref]:
return self._data().xrefs
@xrefs.setter
def xrefs(self, xrefs: Iterable[Xref]):
self._data().xrefs = set(xrefs)