Skip to content

Commit

Permalink
Use a single Relation class; add docs
Browse files Browse the repository at this point in the history
  • Loading branch information
goodmami committed Dec 11, 2024
1 parent 730a5ae commit ebe99df
Show file tree
Hide file tree
Showing 7 changed files with 176 additions and 73 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@

## Added

* `Relation` class ([#216])
* `Sense.relation_map()` method ([#216])
* `Synset.relation_map()` method ([#167], [#216])

Expand Down
61 changes: 61 additions & 0 deletions docs/api/wn.rst
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,7 @@ The Sense Class
.. automethod:: counts
.. automethod:: metadata
.. automethod:: relations
.. automethod:: relation_map
.. automethod:: get_related
.. automethod:: get_related_synsets
.. automethod:: closure
Expand Down Expand Up @@ -221,6 +222,7 @@ The Synset Class
.. automethod:: holonyms
.. automethod:: meronyms
.. automethod:: relations
.. automethod:: relation_map
.. automethod:: get_related
.. automethod:: closure
.. automethod:: relation_paths
Expand Down Expand Up @@ -253,6 +255,65 @@ The Synset Class
Shortcut for :func:`wn.taxonomy.lowest_common_hypernyms`.


The Relation Class
------------------

The :meth:`Sense.relation_map` and :meth:`Synset.relation_map` methods
return a dictionary mapping :class:`Relation` objects to resolved
target senses or synsets. They differ from :meth:`Sense.relations`
and :meth:`Synset.relations` in two main ways:

1. Relation objects map 1-to-1 to their targets instead of to a list
of targets sharing the same relation name.
2. Relation objects encode not just relation names, but also the
identifiers of sources and targets, the lexicons they came from, and
any metadata they have.

One reason why :class:`Relation` objects are useful is for inspecting
relation metadata, particularly in order to distinguish ``other``
relations that differ only by the value of their ``dc:type`` metadata:

>>> oewn = wn.Wordnet('oewn:2024')
>>> alloy = oewn.senses("alloy", pos="v")[0]
>>> alloy.relations() # appears to only have one 'other' relation
{'derivation': [Sense('oewn-alloy__1.27.00..')], 'other': [Sense('oewn-alloy__1.27.00..')]}
>>> for rel in alloy.relation_map(): # but in fact there are two
... print(rel, rel.subtype)
...
Relation('derivation', 'oewn-alloy__2.30.00..', 'oewn-alloy__1.27.00..') None
Relation('other', 'oewn-alloy__2.30.00..', 'oewn-alloy__1.27.00..') material
Relation('other', 'oewn-alloy__2.30.00..', 'oewn-alloy__1.27.00..') result

Another reason why they are useful is to determine the source of a
relation used in :doc:`interlingual queries <../guides/interlingual>`.

>>> es = wn.Wordnet("omw-es", expand="omw-en")
>>> mapa = es.synsets("mapa", pos="n")[0]
>>> rel, tgt = next(iter(mapa.relation_map().items()))
>>> rel, rel.lexicon() # relation comes from omw-en
(Relation('hypernym', 'omw-en-03720163-n', 'omw-en-04076846-n'), <Lexicon omw-en:1.4 [en]>)
>>> tgt, tgt.words(), tgt.lexicon() # target is in omw-es
(Synset('omw-es-04076846-n'), [Word('omw-es-representación-n')], <Lexicon omw-es:1.4 [es]>)

.. autoclass:: Relation

.. attribute:: name

The name of the relation. Also called the relation "type".

.. attribute:: source_id

The identifier of the source entity of the relation.

.. attribute:: target_id

The identifier of the target entity of the relation.

.. autoattribute:: subtype
.. automethod:: lexicon
.. automethod:: metadata


The ILI Class
-------------

Expand Down
2 changes: 2 additions & 0 deletions tests/relations_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,7 @@ def test_synset_relation_map():
assert {rel.target_id for rel in relmap} == {'test-en-0001-n', 'test-en-0004-n'}
# synset relation targets have same ids as resolved targets in same lexicon
assert all(rel.target_id == tgt.id for rel, tgt in relmap.items())
assert all(rel.lexicon().id == 'test-en' for rel in relmap)

# interlingual synset relation targets show original target ids
es = wn.Wordnet('test-es', expand='test-en')
Expand All @@ -162,3 +163,4 @@ def test_synset_relation_map():
assert {rel.name for rel in relmap} == {'hypernym', 'hyponym'}
assert {rel.target_id for rel in relmap} == {'test-en-0001-n', 'test-en-0004-n'}
assert all(rel.target_id != tgt.id for rel, tgt in relmap.items())
assert all(rel.lexicon().id == 'test-en' for rel in relmap)
2 changes: 2 additions & 0 deletions wn/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
'synset',
'synsets',
'Synset',
'Relation',
'ili',
'ilis',
'ILI',
Expand Down Expand Up @@ -53,6 +54,7 @@
word, words, Word, Form, Pronunciation, Tag,
sense, senses, Sense, Count,
synset, synsets, Synset,
Relation,
ili, ilis, ILI,
Wordnet
)
Expand Down
106 changes: 65 additions & 41 deletions wn/_core.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import textwrap
import warnings
from collections.abc import Callable, Iterator, Sequence
from typing import Optional, TypeVar, Union
from typing import Optional, TypeVar

import wn
from wn._types import (
Expand Down Expand Up @@ -463,45 +463,73 @@ def translate(
return result


class _Relation(_LexiconElement):
__slots__ = 'name', 'source_id', 'target_id'
class Relation:
"""A class to model relations between senses or synsets.
Args:
name: the name, or "type", of the relation
source_id: the identifier of the source sense or synset
target_id: the identifier of the target sense or synset
lexicon: the lexicon specifier of the lexicon where the relation
is defined
metadata: metadata associated with the relation
"""
__slots__ = 'name', 'source_id', 'target_id', '_metadata', '_lexicon'
__module__ = 'wn'

def __init__(
self,
name: str,
source_id: str,
target_id: str,
_lexid: int = NON_ROWID,
_id: int = NON_ROWID,
_wordnet: Optional['Wordnet'] = None
lexicon: str,
*,
metadata: Optional[Metadata] = None,
):
super().__init__(_lexid=_lexid, _id=_id, _wordnet=_wordnet)
self.name = name
self.source_id = source_id
self.target_id = target_id
self._lexicon = lexicon
self._metadata: Metadata = metadata or {}

def __repr__(self) -> str:
return (
self.__class__.__name__
+ f"({self.name!r}, {self.source_id!r}, {self.target_id!r})"
)

def metadata(self) -> Metadata:
"""Return the synset's metadata."""
return get_metadata(self._id, self._ENTITY_TYPE)

def __eq__(self, other) -> bool:
if not isinstance(other, Relation):
return NotImplemented
return (
self.name == other.name
and self.source_id == other.source_id
and self.target_id == other.target_id
and self._lexicon == other._lexicon
and self.subtype == other.subtype
)

class SynsetRelation(_Relation):
_ENTITY_TYPE = _EntityType.SYNSET_RELATIONS
def __hash__(self) -> int:
datum = self.name, self.source_id, self.target_id, self._lexicon, self.subtype
return hash(datum)

@property
def subtype(self) -> Optional[str]:
"""
The value of the ``dc:type`` metadata.
class SenseRelation(_Relation):
_ENTITY_TYPE = _EntityType.SENSE_RELATIONS
If ``dc:type`` is not specified in the metadata, ``None`` is
returned instead.
"""
return self._metadata.get("type")

def lexicon(self) -> Lexicon:
"""Return the :class:`Lexicon` where the relation is defined."""
return _to_lexicon(next(find_lexicons(self._lexicon)))

class SenseSynsetRelation(_Relation):
_ENTITY_TYPE = _EntityType.SENSE_SYNSET_RELATIONS
def metadata(self) -> Metadata:
"""Return the relation's metadata."""
return self._metadata


T = TypeVar('T', bound='_Relatable')
Expand Down Expand Up @@ -733,10 +761,11 @@ def get_related(self, *args: str) -> list['Synset']:
"""
return unique_list(synset for _, synset in self._iter_relations(*args))

def relation_map(self) -> dict[SynsetRelation, 'Synset']:
def relation_map(self) -> dict[Relation, 'Synset']:
"""Return a dict mapping :class:`Relation` objects to targets."""
return dict(self._iter_relations())

def _iter_relations(self, *args: str) -> Iterator[tuple[SynsetRelation, 'Synset']]:
def _iter_relations(self, *args: str) -> Iterator[tuple[Relation, 'Synset']]:
# first get relations from the current lexicon(s)
if self._id != NON_ROWID:
yield from self._iter_local_relations(args)
Expand All @@ -747,14 +776,12 @@ def _iter_relations(self, *args: str) -> Iterator[tuple[SynsetRelation, 'Synset'
def _iter_local_relations(
self,
args: Sequence[str],
) -> Iterator[tuple[SynsetRelation, 'Synset']]:
) -> Iterator[tuple[Relation, 'Synset']]:
_wn = self._wordnet
lexids = self._get_lexicon_ids()
iterable = get_synset_relations({self._id}, args, lexids)
for relname, rellexid, relrowid, _, ssid, pos, ili, lexid, rowid in iterable:
synset_rel = SynsetRelation(
relname, self.id, ssid, rellexid, relrowid, _wordnet=_wn
)
for relname, lexicon, metadata, _, ssid, pos, ili, lexid, rowid in iterable:
synset_rel = Relation(relname, self.id, ssid, lexicon, metadata=metadata)
synset = Synset(
ssid,
pos,
Expand All @@ -768,7 +795,7 @@ def _iter_local_relations(
def _iter_expanded_relations(
self,
args: Sequence[str],
) -> Iterator[tuple[SynsetRelation, 'Synset']]:
) -> Iterator[tuple[Relation, 'Synset']]:
_wn = self._wordnet
lexids = self._get_lexicon_ids()
expids = self._wordnet._expanded_ids
Expand All @@ -782,11 +809,11 @@ def _iter_expanded_relations(
}

iterable = get_synset_relations(set(srcids), args, expids)
for relname, rellexid, relrowid, srcrowid, ssid, _, ili, *_ in iterable:
for relname, lexicon, metadata, srcrowid, ssid, _, ili, *_ in iterable:
if ili is None:
continue
synset_rel = SynsetRelation(
relname, srcids[srcrowid], ssid, rellexid, relrowid, _wn
synset_rel = Relation(
relname, srcids[srcrowid], ssid, lexicon, metadata=metadata
)
local_ss_rows = list(get_synsets_for_ilis([ili], lexicon_rowids=lexids))

Expand Down Expand Up @@ -1040,6 +1067,10 @@ def relations(self, *args: str) -> dict[str, list['Sense']]:
# now convert inner dicts to lists
return {relname: list(s_dict) for relname, s_dict in relmap.items()}

def relation_map(self) -> dict[Relation, 'Sense']:
"""Return a dict mapping :class:`Relation` objects to targets."""
return dict(self._iter_sense_relations())

def get_related(self, *args: str) -> list['Sense']:
"""Return a list of related senses.
Expand All @@ -1065,15 +1096,10 @@ def get_related_synsets(self, *args: str) -> list[Synset]:
synset for _, synset in self._iter_sense_synset_relations(*args)
)

def relation_map(self) -> dict[SenseRelation, 'Sense']:
return dict(self._iter_sense_relations())

def _iter_sense_relations(self, *args: str) -> Iterator[tuple[SenseRelation, 'Sense']]:
def _iter_sense_relations(self, *args: str) -> Iterator[tuple[Relation, 'Sense']]:
iterable = get_sense_relations(self._id, args, self._get_lexicon_ids())
for relname, rellexid, relrowid, _, sid, eid, ssid, lexid, rowid in iterable:
relation = SenseRelation(
relname, self.id, sid, rellexid, relrowid, _wordnet=self._wordnet
)
for relname, lexicon, metadata, sid, eid, ssid, lexid, rowid in iterable:
relation = Relation(relname, self.id, sid, lexicon, metadata=metadata)
sense = Sense(
sid, eid, ssid, lexid, rowid, _wordnet=self._wordnet
)
Expand All @@ -1082,12 +1108,10 @@ def _iter_sense_relations(self, *args: str) -> Iterator[tuple[SenseRelation, 'Se
def _iter_sense_synset_relations(
self,
*args: str,
) -> Iterator[tuple[SenseSynsetRelation, 'Synset']]:
) -> Iterator[tuple[Relation, 'Synset']]:
iterable = get_sense_synset_relations(self._id, args, self._get_lexicon_ids())
for relname, rellexid, relrowid, _, sid, pos, ili, lexid, rowid in iterable:
relation = SenseSynsetRelation(
relname, self.id, ssid, rellexid, relrowid, _wordnet=self._wordnet
)
for relname, lexicon, metadata, _, ssid, pos, ili, lexid, rowid in iterable:
relation = Relation(relname, self.id, ssid, lexicon, metadata=metadata)
synset = Synset(
ssid, pos, ili, lexid, rowid, _wordnet=self._wordnet
)
Expand Down
12 changes: 6 additions & 6 deletions wn/_export.py
Original file line number Diff line number Diff line change
Expand Up @@ -212,15 +212,15 @@ def _export_sense_relations(
relations: list[lmf.Relation] = [
{'target': id,
'relType': type,
'meta': _export_metadata(rowid, 'sense_relations')}
for type, _, rowid, _, id, *_
'meta': metadata}
for type, _, metadata, id, *_
in get_sense_relations(sense_rowid, '*', lexids)
]
relations.extend(
{'target': id,
'relType': type,
'meta': _export_metadata(rowid, 'sense_synset_relations')}
for type, _, rowid, _, id, *_
'meta': metadata}
for type, _, metadata, _, id, *_
in get_sense_synset_relations(sense_rowid, '*', lexids)
)
return relations
Expand Down Expand Up @@ -303,8 +303,8 @@ def _export_synset_relations(
return [
{'target': id,
'relType': type,
'meta': _export_metadata(rowid, 'synset_relations')}
for type, _, rowid, _, id, *_
'meta': metadata}
for type, _, metadata, _, id, *_
in get_synset_relations((synset_rowid,), '*', lexids)
]

Expand Down
Loading

0 comments on commit ebe99df

Please sign in to comment.