Repository: cogent3/cogent3
Pull request #2065 (2024-10-26 05:25:50)

ENH: ensure all nodes have names when using `make_tree`, and convert tip names to strings
#2045

## Summary by Sourcery

Enhance the make_tree function to ensure all nodes have names and convert tip names to strings. Add tests to verify these enhancements.

Enhancements:
- Ensure all nodes have names when using the make_tree function.

Tests:
- Add test to verify that tip names are converted to strings when creating a tree from integer tip names.
- Add test to ensure all node names are uniquely assigned and no node name is an empty string.


Patch:
diff --git a/src/cogent3/__init__.py b/src/cogent3/__init__.py
index e33d45b35..1b8e1b40a 100644
--- a/src/cogent3/__init__.py
+++ b/src/cogent3/__init__.py
@@ -31,10 +31,7 @@ from cogent3.core.moltype import (
     get_moltype,
 )
 from cogent3.core.tree import PhyloNode, TreeBuilder, TreeError, TreeNode
-from cogent3.evolve.fast_distance import (
-    available_distances,
-    get_distance_calculator,
-)
+from cogent3.evolve.fast_distance import available_distances, get_distance_calculator
 from cogent3.evolve.models import available_models, get_model
 from cogent3.parse.cogent3_json import load_from_json
 from cogent3.parse.newick import parse_string as newick_parse_string
@@ -686,7 +683,13 @@ def load_table(
     )
 
 
-def make_tree(treestring=None, tip_names=None, format=None, underscore_unmunge=False):
+def make_tree(
+    treestring=None,
+    tip_names=None,
+    format=None,
+    underscore_unmunge=False,
+    name_nodes=False,
+):
     """Initialises a tree.
 
     Parameters
@@ -701,6 +704,8 @@ def make_tree(treestring=None, tip_names=None, format=None, underscore_unmunge=F
     underscore_unmunge : bool
         replace underscores with spaces in all names read, i.e. "sp_name"
         becomes "sp name"
+    name_nodes: bool
+        whether to name unnamed nodes
 
     Notes
     -----
@@ -714,7 +719,7 @@ def make_tree(treestring=None, tip_names=None, format=None, underscore_unmunge=F
     assert treestring or tip_names, "must provide either treestring or tip_names"
     if tip_names:
         tree_builder = TreeBuilder().create_edge
-        tips = [tree_builder([], tip_name, {}) for tip_name in tip_names]
+        tips = [tree_builder([], str(tip_name), {}) for tip_name in tip_names]
         tree = tree_builder(tips, "root", {})
         return tree
 
@@ -730,6 +735,10 @@ def make_tree(treestring=None, tip_names=None, format=None, underscore_unmunge=F
     if not tree.name_loaded:
         tree.name = "root"
 
+    # ensure all nodes have names if name_nodes is True
+    if name_nodes:
+        tree.name_unnamed_nodes()
+
     return tree
 
 


Test patch:
diff --git a/tests/test_core/test_tree.py b/tests/test_core/test_tree.py
index d054bc795..d5d4026d5 100644
--- a/tests/test_core/test_tree.py
+++ b/tests/test_core/test_tree.py
@@ -14,7 +14,8 @@ from numpy.testing import assert_allclose, assert_equal
 
 from cogent3 import load_tree, make_tree, open_
 from cogent3._version import __version__
-from cogent3.core.tree import PhyloNode, TreeError, TreeNode, split_name_and_support
+from cogent3.core.tree import (PhyloNode, TreeError, TreeNode,
+                               split_name_and_support)
 from cogent3.maths.stats.test import correlation
 from cogent3.parse.tree import DndParser
 from cogent3.util.misc import get_object_provenance
@@ -49,6 +50,10 @@ class TreeTests(TestCase):
         self.assertEqual(names, ["a a", "b b", "c c"])
         self.assertEqual(str(t), result_str)
         self.assertEqual(t.get_newick(with_distances=True), result_str)
+        # ensure tip names are converted to strings
+        # when creating a tree from a list of integer tip names.
+        t = make_tree(tip_names=[1, 2, 3])
+        self.assertEqual(t.get_tip_names(), ["1", "2", "3"])
 
 
 def _new_child(old_node, constructor):
@@ -2380,3 +2385,20 @@ def test_phylonode_support():
     name_and_support = tree.get_node_matching_name("def")
     assert name_and_support.name == "def"  # bit redundant given selection process
     assert name_and_support.params["support"] == 25.0
+
+
+def test_phylonode_support_name_nodes_false():
+    # test that internal node names are None with name_nodes=False
+    tree = make_tree("((1,2)5,(3,4)6);", name_nodes=False)
+    internal_nodes = [node.name for node in tree.iter_nontips()]
+    assert all(node is None for node in internal_nodes)
+
+
+def test_phylonode_support_name_nodes_true():
+    # test that all nodes have unique names with name_nodes=True
+    tree = make_tree("((1,2)5,(3,4)6);", name_nodes=True)
+    node_names = set(tree.get_node_names())
+    # check that no node name is an empty string or None
+    assert all(node_names)
+    # check that the total number of unique node names is 7
+    assert len(node_names) == 7