import hpotk
import abc
import enum
import typing
from ._base import Identified, Named
from ._term_id import TermId
[docs]
class MinimalTerm(Identified, Named, metaclass=abc.ABCMeta):
"""
`MinimalTerm` is a data object with the minimal useful information about an ontology concept.
Each term has:
* `identifier` - a :class:`TermId` of the term (thanks to inheriting from :class:`Identified`)
* `name` - a human-friendly name of the term (thanks to inheriting from :class:`Named`)
* `alt_term_ids` - a sequence of *alternate identifiers* (IDs of obsolete terms that should be replaced by this term)
* `is_obsolete` - the *obsoletion status*
Most of the time, you should get terms from an :class:`hpotk.ontology.MinimalOntology`. However, `MinimalTerm` can
also be created from scratch using :func:`create_minimal_term` if you must do that from whatever reason.
"""
[docs]
@staticmethod
def create_minimal_term(term_id: typing.Union[TermId, str],
name: str,
alt_term_ids: typing.Iterable[typing.Union[TermId, str]],
is_obsolete: bool):
"""
Create `MinimalTerm` from the components.
.. doctest::
>>> seizure = MinimalTerm.create_minimal_term(term_id='HP:0001250', name='Seizure',
... alt_term_ids=('HP:0002279', 'HP:0002391'),
... is_obsolete=False)
:param term_id: a `TermId` or a CURIE `str` (e.g. 'HP:0001250').
:param name: term name (e.g. Seizure) .
:param alt_term_ids: an iterable with term IDs that represent the alternative IDs of the term.
:param is_obsolete: `True` if the `MinimalTerm` has been obsoleted, or `False` otherwise.
:return: the created term.
"""
return DefaultMinimalTerm(term_id, name, alt_term_ids, is_obsolete)
@property
@abc.abstractmethod
def alt_term_ids(self) -> typing.Sequence[TermId]:
"""
Get a sequence of identifiers of the ontology concepts that were obsoleted and should be replaced by
the concept represented by this `Term`.
"""
pass
@property
def is_current(self) -> bool:
"""
Return `True` if the term is current (*not* obsolete) and `False` otherwise.
"""
return not self.is_obsolete
@property
@abc.abstractmethod
def is_obsolete(self) -> bool:
"""
Return `True` if the term is obsolete (*not* current) and `False` otherwise.
"""
pass
def __eq__(self, other):
return isinstance(other, MinimalTerm) \
and self.identifier == other.identifier \
and self.name == other.name \
and self.alt_term_ids == other.alt_term_ids \
and self.is_obsolete == other.is_obsolete
def __str__(self):
return f'MinimalTerm(' \
f'identifier="{self.identifier}", ' \
f'name="{self.name}", ' \
f'alt_term_ids={self.alt_term_ids}, ' \
f'is_obsolete={self.is_obsolete})'
[docs]
class SynonymCategory(enum.Enum):
"""
An enumeration of the synonym categories.
"""
EXACT = enum.auto()
RELATED = enum.auto()
BROAD = enum.auto()
NARROW = enum.auto()
[docs]
class SynonymType(enum.Enum):
"""
An enumeration of the synonym types, as provided by Obographs.
"""
LAYPERSON_TERM = enum.auto()
ABBREVIATION = enum.auto()
UK_SPELLING = enum.auto()
OBSOLETE_SYNONYM = enum.auto()
PLURAL_FORM = enum.auto()
ALLELIC_REQUIREMENT = enum.auto()
# We may not need these, unless we support ECTO
# IUPAC_NAME = enum.auto()
# INN = enum.auto()
# BRAND_NAME = enum.auto()
# IN_PART = enum.auto()
# SYNONYM = enum.auto()
# BLAST_NAME = enum.auto()
# GENBANK_COMMON_NAME = enum.auto()
# COMMON_NAME = enum.auto()
[docs]
def is_obsolete(self) -> bool:
"""
Returns `True` if the synonym is obsolete (not current) and `False` otherwise.
"""
return self == SynonymType.OBSOLETE_SYNONYM
[docs]
def is_current(self) -> bool:
"""
Returns `True` if the synonym is current (not obsolete) and `False` otherwise.
"""
return not self.is_obsolete()
[docs]
class Synonym(Named):
"""
`Synonym` represents the information regarding a synonym of an ontology concept.
"""
def __init__(self, name: str,
synonym_category: typing.Optional[SynonymCategory] = None,
synonym_type: typing.Optional[SynonymType] = None,
xrefs: typing.Optional[typing.Sequence[TermId]] = None):
self._name = name
self._scat = synonym_category
self._stype = synonym_type
self._xrefs = xrefs
@property
def name(self) -> str:
"""
Get the name of the synonym
"""
return self._name
@property
def category(self) -> typing.Optional[SynonymCategory]:
"""
Get the synonym category - an instance of :class:`SynonymCategory` or ``None``.
"""
return self._scat
@property
def synonym_type(self) -> typing.Optional[SynonymType]:
"""
Get synonym type - an instance of :class:`SynonymType` or ``None``.
"""
return self._stype
@property
def xrefs(self) -> typing.Optional[typing.Sequence[TermId]]:
"""
Get a sequence of identifiers of the cross-references of the ontology concept or ``None`` if there are none.
"""
return self._xrefs
def __eq__(self, other):
return isinstance(other, Synonym) \
and self.name == other.name \
and self.category == other.category \
and self.synonym_type == other.synonym_type \
and self.xrefs == other.xrefs
def __str__(self):
return f'Synonym(' \
f'name="{self.name}", ' \
f'category={self.category}, ' \
f'synonym_type={self.synonym_type}, ' \
f'xrefs="{self.xrefs}")'
def __repr__(self):
return str(self)
[docs]
class Definition:
"""
`Definition` includes a definition and the cross-references.
:param definition: a definition,
e.g. *Abnormally long and slender fingers ("spider fingers").* for *Arachnodactyly*.
:param xrefs: an iterable with definition cross-references,
e.g. `('https://orcid.org/0000-0002-0736-9199',)` for *Arachnodactyly*.
"""
def __init__(
self,
definition: str,
xrefs: typing.Iterable[str],
):
self._definition = hpotk.util.validate_instance(definition, str, 'definition')
self._xrefs = tuple(xrefs)
@property
def definition(self) -> str:
"""
Get a `str` with the term definition.
For instance, *Abnormally long and slender fingers ("spider fingers").* for *Arachnodactyly*.
"""
return self._definition
@property
def xrefs(self) -> typing.Sequence[str]:
"""
Get definition of cross-references of a definition.
For instance, `('https://orcid.org/0000-0002-0736-9199',)` for *Arachnodactyly*.
"""
return self._xrefs
def __eq__(self, other):
return isinstance(other, Definition) \
and self.definition == other.definition \
and self.xrefs == other.xrefs
def __str__(self):
return f'Definition(' \
f'definition="{self.definition}", ' \
f'xrefs={self.xrefs}")'
def __repr__(self):
return str(self)
[docs]
class Term(MinimalTerm, metaclass=abc.ABCMeta):
"""
A comprehensive representation of an ontology concept.
`Term` has all attributes of the :class:`MinimalTerm` plus the following:
* `definition` - an optional definition of the term, including a comprehensive description and cross-references
* `comment` - an optional comment
* `synonyms` - an optional sequence of term synonyms
* `cross-references` - an optional sequence of cross-references
Most of the time, you should be getting terms from :class:`hpotk.ontology.Ontology`.
However, if you absolutely must craft a term or two by hand, use :func:`create_term` function.
"""
# TODO - add the remaining attributes from phenol's Term?
[docs]
@staticmethod
def create_term(identifier: typing.Union[TermId, str],
name: str,
alt_term_ids: typing.Iterable[typing.Union[TermId, str]],
is_obsolete: bool,
definition: typing.Optional[typing.Union[Definition, str]],
comment: typing.Optional[str],
synonyms: typing.Optional[typing.Iterable[Synonym]],
xrefs: typing.Optional[typing.Iterable[TermId]]):
"""
Create a `MinimalTerm` from the components.
:param identifier: a `TermId` or a CURIE (e.g. 'HP:0001250').
:param name: term name (e.g. Seizure).
:param alt_term_ids: an iterable with term IDs that represent the alternative IDs of the term.
:param is_obsolete: `True` if the `MinimalTerm` has been obsoleted, or `False` otherwise.
:param definition: an optional `str` with a definition of the term or a :class:`Definition` with the full info.
:param comment: an optional comment of the term.
:param synonyms: an optional iterable with all synonyms of the term.
:param xrefs: an optional iterable with all the cross-references.
:return: the created term.
"""
if isinstance(definition, str):
definition = Definition(definition, ())
return DefaultTerm(identifier, name, alt_term_ids, is_obsolete, definition, comment, synonyms, xrefs)
@property
@abc.abstractmethod
def definition(self) -> typing.Optional[Definition]:
"""
Get the definition of the ontology concept.
"""
pass
@property
@abc.abstractmethod
def comment(self) -> typing.Optional[str]:
"""
Get the comment string of the ontology concept.
"""
pass
@property
@abc.abstractmethod
def synonyms(self) -> typing.Optional[typing.Sequence[Synonym]]:
"""
Get a sequence of all synonyms (including obsolete) of the ontology concept or `None` if the concept
has no synonyms.
"""
pass
[docs]
def current_synonyms(self) -> typing.Iterable[Synonym]:
"""
Get an iterable with *current* synonyms of the ontology concept.
The iterable is empty if the concept has no current synonyms.
"""
return self._synonyms_iter(lambda synonym:
synonym.synonym_type is None
or synonym.synonym_type.is_current())
[docs]
def obsolete_synonyms(self) -> typing.Iterable[Synonym]:
"""
Get an iterable with *obsolete* synonyms of the ontology concept.
The iterable is empty if the concept has no obsolete synonyms.
"""
return self._synonyms_iter(lambda synonym:
synonym.synonym_type is not None
and synonym.synonym_type.is_obsolete())
def _synonyms_iter(self, filter_f):
if self.synonyms is None:
return ()
else:
for synonym in self.synonyms:
if filter_f(synonym):
yield synonym
@property
@abc.abstractmethod
def xrefs(self) -> typing.Optional[typing.Sequence[TermId]]:
"""
Get a sequence of the cross-references of the ontology concept.
"""
pass
def __eq__(self, other):
return MinimalTerm.__eq__(self, other) \
and isinstance(other, Term) \
and self.definition == other.definition \
and self.comment == other.comment \
and self.synonyms == other.synonyms \
and self.xrefs == other.xrefs
def __str__(self):
return f'Term(' \
f'identifier={self.identifier}, ' \
f'name="{self.name}", ' \
f'definition={self.definition}, ' \
f'comment={self.comment}, ' \
f'synonyms={self.synonyms}, ' \
f'xrefs={self.xrefs}, ' \
f'is_obsolete={self.is_obsolete}, ' \
f'alt_term_ids="{self.alt_term_ids}")'
def map_to_term_id(value: typing.Union[TermId, str]) -> TermId:
if isinstance(value, TermId):
return value
elif isinstance(value, str):
return TermId.from_curie(value)
else:
raise ValueError(f'Expected a `TermId` or `str` but got {type(value)}')
def validate_name(name: typing.Optional[str]) -> str:
# Some obsolete nodes do not have labels in the Obographs format.
# We assign an empty string.
if name is None:
return ''
else:
return hpotk.util.validate_instance(name, str, 'name')
class DefaultMinimalTerm(MinimalTerm):
def __init__(self, identifier: typing.Union[TermId, str],
name: str,
alt_term_ids: typing.Iterable[typing.Union[TermId, str]],
is_obsolete: bool):
self._id = hpotk.util.validate_instance(map_to_term_id(identifier), TermId, 'identifier')
self._name = validate_name(name)
self._alts = tuple(map(map_to_term_id, alt_term_ids))
self._is_obsolete = hpotk.util.validate_instance(is_obsolete, bool, 'is_obsolete')
@property
def identifier(self) -> TermId:
return self._id
@property
def name(self) -> str:
return self._name
@property
def alt_term_ids(self) -> typing.Sequence[TermId]:
return self._alts
@property
def is_obsolete(self) -> bool:
return self._is_obsolete
def __repr__(self):
return f'DefaultMinimalTerm(' \
f'identifier={self._id},' \
f' name="{self._name}",' \
f' is_obsolete={self._is_obsolete},' \
f' alt_term_ids="{self._alts}")'
def validate_synonyms(synonyms: typing.Optional[typing.Iterable[Synonym]]):
if synonyms is None:
return None
else:
validated = []
for i, s in enumerate(synonyms):
validated.append(hpotk.util.validate_instance(s, Synonym, f'synonym #{i}'))
return tuple(validated)
class DefaultTerm(DefaultMinimalTerm, Term):
def __init__(self, identifier: typing.Union[TermId, str],
name: str,
alt_term_ids: typing.Iterable[typing.Union[TermId, str]],
is_obsolete: bool,
definition: typing.Optional[Definition],
comment: typing.Optional[str],
synonyms: typing.Optional[typing.Iterable[Synonym]],
xrefs: typing.Optional[typing.Iterable[typing.Union[TermId, str]]]):
DefaultMinimalTerm.__init__(self, identifier=identifier, name=name,
alt_term_ids=alt_term_ids, is_obsolete=is_obsolete)
self._definition = hpotk.util.validate_optional_instance(definition, Definition, 'definition')
self._comment = hpotk.util.validate_optional_instance(comment, str, 'comment')
self._synonyms = validate_synonyms(synonyms)
self._xrefs = tuple(map(map_to_term_id, xrefs)) if xrefs is not None else None
@property
def definition(self) -> typing.Optional[Definition]:
return self._definition
@property
def comment(self) -> typing.Optional[str]:
return self._comment
@property
def synonyms(self) -> typing.Optional[typing.Sequence[Synonym]]:
return self._synonyms
@property
def xrefs(self) -> typing.Optional[typing.Sequence[TermId]]:
return self._xrefs
def __repr__(self):
return f'DefaultTerm(' \
f'identifier={self._id}, ' \
f'name="{self._name}", ' \
f'definition={self._definition}, ' \
f'comment={self._comment}, ' \
f'synonyms={self._synonyms}, ' \
f'xrefs={self._xrefs}, ' \
f'is_obsolete={self._is_obsolete}, ' \
f'alt_term_ids="{self._alts}")'