Repository: cogent3/cogent3
Pull request #2054 (2024-10-22 21:11:11)

ENH: extract support measures on tree nodes from newick strings
[NEW] handled automatically while parsing, uses the new `split_name_and_support()`
    function.

[NEW] added scriv changelog snippet

[CHANGED] modernised length property and no longer inherit from
    object

## Summary by Sourcery

Enhance the tree parsing functionality to extract and store node support measures from Newick strings. Modernize the TreeNode class by removing the object inheritance and updating the length property. Add tests for the new parsing functionality and update the documentation with a changelog entry.

New Features:
- Add support for parsing node support measures from Newick strings in tree objects.

Enhancements:
- Modernize the TreeNode class by removing inheritance from object and updating the length property to use Python's property decorator.

Documentation:
- Add a new changelog entry describing the enhancement of parsing node support from Newick format.

Tests:
- Add tests for the new functionality of parsing node support measures from Newick strings.


Patch:
diff --git a/changelog.d/20241023_080011_Gavin.Huttley.md b/changelog.d/20241023_080011_Gavin.Huttley.md
new file mode 100644
index 000000000..5a02e0567
--- /dev/null
+++ b/changelog.d/20241023_080011_Gavin.Huttley.md
@@ -0,0 +1,41 @@
+<!--
+A new scriv changelog fragment.
+
+Uncomment the section that is right (remove the HTML comment wrapper).
+-->
+
+### Contributors
+
+- @YapengLang added the key function for parsing support from newick format
+  to tree object 💪
+
+
+### ENH
+
+- cogent3 now supports parsing from newick format with node support
+  statistics. The support measure is stored in `node.params["support"]`.
+
+<!--
+### BUG
+
+- A bullet item for the BUG category.
+
+-->
+<!--
+### DOC
+
+- A bullet item for the DOC category.
+
+-->
+<!--
+### Deprecations
+
+- A bullet item for the Deprecations category.
+
+-->
+<!--
+### Discontinued
+
+- A bullet item for the Discontinued category.
+
+-->
diff --git a/src/cogent3/core/tree.py b/src/cogent3/core/tree.py
index d12319495..3bdbf6c34 100644
--- a/src/cogent3/core/tree.py
+++ b/src/cogent3/core/tree.py
@@ -36,6 +36,7 @@ from functools import reduce
 from itertools import combinations
 from operator import or_
 from random import choice, shuffle
+from typing import Union
 
 from numpy import argsort, ceil, log, zeros
 
@@ -69,7 +70,7 @@ def _copy_node(n):
     return result
 
 
-class TreeNode(object):
+class TreeNode:
     """Store information about a tree node. Mutable.
 
     Parameters:
@@ -1693,22 +1694,27 @@ class PhyloNode(TreeNode):
     def __init__(self, *args, **kwargs):
         length = kwargs.get("length", None)
         params = kwargs.get("params", {})
-        if "length" not in params:
-            params["length"] = length
+        params["length"] = params.get("length", length)
         kwargs["params"] = params
-        super(PhyloNode, self).__init__(*args, **kwargs)
-        # split name into name/support here
+        super().__init__(*args, **kwargs)
 
-    def _set_length(self, value):
+        if self.children:
+            # self is an internal tree node
+            name, support = split_name_and_support(self.name)
+            self.name = name
+            if support is not None:
+                self.params["support"] = support
+
+    @property
+    def length(self) -> Union[float, None]:
+        return self.params.get("length", None)
+
+    @length.setter
+    def length(self, value: Union[float, None]) -> None:
         if not hasattr(self, "params"):
             self.params = {}
         self.params["length"] = value
 
-    def _get_length(self):
-        return self.params.get("length", None)
-
-    length = property(_get_length, _set_length)
-
     def __str__(self):
         """Returns string version of self, with names and distances."""
         return self.get_newick(with_distances=True)
@@ -1773,7 +1779,7 @@ class PhyloNode(TreeNode):
         to_process = [(self, 0.0)]
         tips_to_save = []
 
-        seen = set([id(self)])
+        seen = {id(self)}
         while to_process:
             curr_node, curr_dist = to_process.pop(0)
 


Test patch:
diff --git a/tests/test_core/test_tree.py b/tests/test_core/test_tree.py
index c4bb629e3..28a2c0f63 100644
--- a/tests/test_core/test_tree.py
+++ b/tests/test_core/test_tree.py
@@ -2352,3 +2352,23 @@ def test_split_name_and_support(name, expected):
 def test_split_name_and_support_invalid_support(invalid):
     with pytest.raises(ValueError):
         split_name_and_support(invalid)
+
+
+def test_phylonode_support():
+    tip_names = [str(i) for i in range(1, 13)]
+    tree = make_tree(
+        treestring="(1,(((2,3),4)/53,(5,((6,(7,(8,9))def/25),(10,11)abc))),12);"
+    )
+    assert tree.get_tip_names() == tip_names
+    # parent of 4 is node with only support value
+    just_support = tree.get_node_matching_name("4").parent
+    assert just_support.params["support"] == 53.0
+    # parent of 10 has a node name only
+    just_name = tree.get_node_matching_name("10").parent
+    assert just_name.name == "abc"
+    assert "support" not in just_name.params
+    # the node with name "def/25" correctly resoloved into node
+    # name "def" and support 25.0
+    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