Skip to content

Commit

Permalink
safer add_node & add_edge methods
Browse files Browse the repository at this point in the history
networkx add_node silently updates existing nodes.
networkx add_edge silently addes missing nodes and updates existing
edges.

Add NXOntology methods that raise errors for these dangerous
situations.
  • Loading branch information
dhimmel committed Dec 4, 2020
1 parent 7809149 commit 658e063
Show file tree
Hide file tree
Showing 4 changed files with 76 additions and 1 deletion.
3 changes: 2 additions & 1 deletion nxontology/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
from nxontology import exceptions
from nxontology.ontology import NXOntology

__all__ = ["NXOntology"]
__all__ = ["NXOntology", "exceptions"]
18 changes: 18 additions & 0 deletions nxontology/exceptions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import networkx.exception as nx_exception


class NXO_Exception(Exception):
"""Base class for exceptions in nxontology."""


class DuplicateError(NXO_Exception):
"""
A node or edge is being added to the graph when it already exists.
NetworkX add_* methods silently update existing nodes or edges,
but duplicate additions often arise from dirty input data
that should be explicitly addressed before graph creation.
"""


class NodeNotFound(NXO_Exception, nx_exception.NodeNotFound): # type: ignore [misc]
"""Exception raised if requested node is not present in the graph."""
26 changes: 26 additions & 0 deletions nxontology/ontology.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@
import networkx as nx
from networkx.readwrite.json_graph import node_link_data, node_link_graph

from .exceptions import DuplicateError, NodeNotFound


class Freezable(abc.ABC):
@property
Expand Down Expand Up @@ -104,6 +106,30 @@ def read_node_link_json(cls, path: str) -> "NXOntology":
nxo = cls(digraph)
return nxo

def add_node(self, node_for_adding: Node, **attr: Any) -> None:
"""
Like networkx.DiGraph.add_node but raises a DuplicateError
if the node already exists.
"""
if node_for_adding in self.graph:
raise DuplicateError(f"node already in graph: {node_for_adding}")
self.graph.add_node(node_for_adding, **attr)

def add_edge(self, u_of_edge: Node, v_of_edge: Node, **attr: Any) -> None:
"""
Like networkx.DiGraph.add_edge but
raises a NodeNotFound if either node does not exist
or a DuplicateError if the edge already exists.
Edge should from general to specific,
such that `u_of_edge` is a parent/superterm/hypernym of `v_of_edge`.
"""
for node in u_of_edge, v_of_edge:
if node not in self.graph:
raise NodeNotFound(f"node does not exist in graph: {node}")
if self.graph.has_edge(u_of_edge, v_of_edge):
raise DuplicateError(f"edge already in graph: {u_of_edge} --> {v_of_edge}")
self.graph.add_edge(u_of_edge, v_of_edge, **attr)

@property # type: ignore [misc]
@cache_on_frozen
def roots(self) -> "Node_Set":
Expand Down
30 changes: 30 additions & 0 deletions nxontology/tests/ontology_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import pytest

from nxontology.examples import create_metal_nxo
from nxontology.exceptions import DuplicateError, NodeNotFound
from nxontology.ontology import Node_Info, NXOntology, Similarity, SimilarityIC


Expand All @@ -27,6 +28,35 @@ def metal_nxo_frozen() -> NXOntology:
return metal_nxo


def test_add_node(metal_nxo: NXOntology) -> None:
assert "brass" not in metal_nxo.graph
metal_nxo.add_node("brass", color="#b5a642")
assert "brass" in metal_nxo.graph
assert metal_nxo.graph.nodes["brass"]["color"] == "#b5a642"


def test_add_node_duplicate(metal_nxo: NXOntology) -> None:
with pytest.raises(DuplicateError):
metal_nxo.add_node("gold")


def test_add_edge(metal_nxo: NXOntology) -> None:
metal_nxo.add_edge("metal", "gold", note="already implied")
assert metal_nxo.graph.has_edge("metal", "gold")
assert metal_nxo.graph.edges["metal", "gold"]["note"] == "already implied"


def test_add_edge_missing_node(metal_nxo: NXOntology) -> None:
assert "brass" not in metal_nxo.graph
with pytest.raises(NodeNotFound):
metal_nxo.add_edge("coinage", "brass")


def test_add_edge_duplicate(metal_nxo: NXOntology) -> None:
with pytest.raises(DuplicateError):
metal_nxo.add_edge("coinage", "gold")


def test_nxontology_read_write_node_link_json(
metal_nxo: NXOntology, tmp_path: pathlib.Path
) -> None:
Expand Down

0 comments on commit 658e063

Please sign in to comment.