From c40c724ea639e9a2051d4ce056b353f84118cec5 Mon Sep 17 00:00:00 2001 From: Antoine Lambert Date: Thu, 16 Nov 2023 21:41:04 +0100 Subject: [PATCH] talipot-python: Add a new Trvial Graph Format import plugin See https://en.wikipedia.org/wiki/Trivial_Graph_Format. --- .../import/TrivialGraphFormatImport.py | 80 +++++++++++ tests/python/CMakeLists.txt | 1 + tests/python/test_tgf_import.py | 128 ++++++++++++++++++ 3 files changed, 209 insertions(+) create mode 100644 library/talipot-python/plugins/import/TrivialGraphFormatImport.py create mode 100644 tests/python/test_tgf_import.py diff --git a/library/talipot-python/plugins/import/TrivialGraphFormatImport.py b/library/talipot-python/plugins/import/TrivialGraphFormatImport.py new file mode 100644 index 0000000000..d24cad6758 --- /dev/null +++ b/library/talipot-python/plugins/import/TrivialGraphFormatImport.py @@ -0,0 +1,80 @@ +# Copyright (C) 2023 The Talipot developers +# +# Talipot is a fork of Tulip, created by David Auber +# and the Tulip development Team from LaBRI, University of Bordeaux +# +# See the AUTHORS file at the top-level directory of this distribution +# License: GNU General Public License version 3, or any later version +# See top-level LICENSE file for more information + +# That plugin imports a graph from a file in the Trivial Graph Format (TGF) +# (see https://en.wikipedia.org/wiki/Trivial_Graph_Format) + +import talipotplugins + +from talipot import tlp + + +class TrivialGraphFormatImport(tlp.ImportModule): + def __init__(self, context): + tlp.ImportModule.__init__(self, context) + self.addFileParameter("filename", True, "The TGF file to import") + + def fileExtensions(self): + return ["tgf"] + + def parseTGF(self, tgfFile): + parsingEdges = False + nodesMap = {} + for line in tgfFile: + line = line.rstrip() + if not line: + continue + if line.startswith("#"): + parsingEdges = True + continue + if not parsingEdges: + split = line.split(" ", maxsplit=1) + n = self.graph.addNode() + nodesMap[split[0]] = n + self.graph["viewLabel"][n] = split[1] if len(split) > 1 else split[0] + + else: + split = line.split(" ", maxsplit=2) + if len(split) < 2: + raise ValueError(f"Invalid edge definition: {line}") + for i in range(2): + if split[i] not in nodesMap: + raise ValueError( + f"Invalid node identifier found when parsing edges: {split[i]}" + ) + e = self.graph.addEdge(nodesMap[split[0]], nodesMap[split[1]]) + if len(split) > 2: + self.graph["viewLabel"][e] = split[2] + + def importGraph(self): + # parse the TGF file + with open(self.dataSet["filename"], "r") as tgfFile: + self.parseTGF(tgfFile) + + return True + + +pluginDoc = """ +

Supported extension: tgf

Imports a graph from a file in the +Trivial Graph Format (hhttps://en.wikipedia.org/wiki/Trivial_Graph_Format). +The format consists of a list of node definitions, which map node IDs to labels, +followed by a list of edges, which specify node pairs and an optional edge label.

+""" + +# The line below does the magic to register the plugin to the plugin database +# and updates the GUI to make it accessible through the menus. +talipotplugins.registerPluginOfGroup( + "TrivialGraphFormatImport", + "Trivial Graph Format (TGF)", + "Antoine Lambert", + "16/11/2023", + pluginDoc, + "1.0", + "File", +) diff --git a/tests/python/CMakeLists.txt b/tests/python/CMakeLists.txt index d0f25519bf..cae2a0968c 100644 --- a/tests/python/CMakeLists.txt +++ b/tests/python/CMakeLists.txt @@ -2,3 +2,4 @@ UNIT_TEST(PythonGraphStructureTest test_graph_structure) UNIT_TEST(PythonGraphPropertiesTest test_graph_properties) UNIT_TEST(PythonPluginParametersTest test_plugin_registration_and_parameters) UNIT_TEST(PythonTalipotPluginsTest test_talipot_plugins) +UNIT_TEST(PythonTGFImportTest test_tgf_import) diff --git a/tests/python/test_tgf_import.py b/tests/python/test_tgf_import.py new file mode 100644 index 0000000000..4b89fda552 --- /dev/null +++ b/tests/python/test_tgf_import.py @@ -0,0 +1,128 @@ +# Copyright (C) 2023 The Talipot developers +# +# Talipot is a fork of Tulip, created by David Auber +# and the Tulip development Team from LaBRI, University of Bordeaux +# +# See the AUTHORS file at the top-level directory of this distribution +# License: GNU General Public License version 3, or any later version +# See top-level LICENSE file for more information + +import os +import tempfile +import textwrap +import unittest + +from talipot import tlp + + +class TestTalipotPlugins(unittest.TestCase): + @classmethod + def setUpClass(cls): + cls.tgfPluginName = "Trivial Graph Format (TGF)" + if "TALIPOT_BUILD_DIR" in os.environ: + talipot_build_dir = os.environ["TALIPOT_BUILD_DIR"] + if talipot_build_dir: + tlp.loadPluginsFromDir( + os.path.join(talipot_build_dir, "library/talipot-python") + ) + + def test_tgf_plugin_loaded(self): + self.assertTrue("Trivial Graph Format (TGF)" in tlp.getImportPluginsList()) + + def test_load_tgf_graph_no_labels(self): + with tempfile.NamedTemporaryFile() as tgfFile: + with open(tgfFile.name, "w") as tgfFileWriter: + tgfFileWriter.write( + textwrap.dedent( + """ + 0 + 1 + 2 + 3 + # + 0 1 + 0 2 + 1 2 + 1 3 + 3 0 + """ + ) + ) + graph = tlp.importGraph(self.tgfPluginName, {"filename": tgfFile.name}) + self.assertTrue(graph is not None) + self.assertEqual(graph.numberOfNodes(), 4) + self.assertEqual(graph.numberOfEdges(), 5) + nodes = graph.nodes() + self.assertTrue(graph.existEdge(nodes[0], nodes[1])) + self.assertTrue(graph.existEdge(nodes[0], nodes[2])) + self.assertTrue(graph.existEdge(nodes[1], nodes[2])) + self.assertTrue(graph.existEdge(nodes[1], nodes[3])) + self.assertTrue(graph.existEdge(nodes[3], nodes[0])) + self.assertEqual( + [graph["viewLabel"][n] for n in graph.nodes()], ["0", "1", "2", "3"] + ) + + def test_load_tgf_graph_with_labels(self): + with tempfile.NamedTemporaryFile() as tgfFile: + with open(tgfFile.name, "w") as tgfFileWriter: + tgfFileWriter.write( + textwrap.dedent( + """ + 0 Node 0 + 1 Node 1 + 2 Node 2 + 3 Node 3 + # + 0 1 Edge 0 + 0 2 Edge 1 + 1 2 Edge 2 + 1 3 Edge 3 + 3 0 Edge 4 + """ + ) + ) + graph = tlp.importGraph(self.tgfPluginName, {"filename": tgfFile.name}) + self.assertEqual( + [graph["viewLabel"][n] for n in graph.nodes()], + [f"Node {i}" for i in range(4)], + ) + self.assertEqual( + [graph["viewLabel"][e] for e in graph.edges()], + [f"Edge {i}" for i in range(5)], + ) + + def test_load_tgf_graph_invalid_edge(self): + with tempfile.NamedTemporaryFile() as tgfFile: + with open(tgfFile.name, "w") as tgfFileWriter: + tgfFileWriter.write( + textwrap.dedent( + """ + 0 + 1 + 2 + 3 + # + 4 + """ + ) + ) + graph = tlp.importGraph(self.tgfPluginName, {"filename": tgfFile.name}) + self.assertTrue(graph is None) + + def test_load_tgf_graph_invalid_node_id(self): + with tempfile.NamedTemporaryFile() as tgfFile: + with open(tgfFile.name, "w") as tgfFileWriter: + tgfFileWriter.write( + textwrap.dedent( + """ + 0 + 1 + 2 + 3 + # + 4 5 + """ + ) + ) + graph = tlp.importGraph(self.tgfPluginName, {"filename": tgfFile.name}) + self.assertTrue(graph is None)