Skip to content

Commit 811a4a9

Browse files
Copilottanghaibao
andauthored
Migrate ete3 → ete4 for Python 3.13+ compatibility (#829)
* Initial plan * Migrate from ete3 to ete4 for Python 3.13+ compatibility Co-authored-by: tanghaibao <106987+tanghaibao@users.noreply.github.com> * Style fixes by Black --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: tanghaibao <106987+tanghaibao@users.noreply.github.com>
1 parent 22148d1 commit 811a4a9

6 files changed

Lines changed: 101 additions & 28 deletions

File tree

pyproject.toml

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ dependencies = [
3737
"CrossMap",
3838
"cython",
3939
"deap",
40-
"ete3",
40+
"ete4",
4141
"ftpretty",
4242
"genomepy",
4343
"gffutils",
@@ -125,7 +125,6 @@ addopts = "-v --cov=jcvi --cov-branch --cov-report=xml --cov-report=term"
125125
testpaths = ["tests"]
126126
python_files = ["test_*.py"]
127127
filterwarnings = [
128-
"ignore:invalid escape sequence.*:SyntaxWarning:ete3\\..*",
129128
]
130129

131130
[tool.isort]

src/jcvi/apps/phylo.py

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@
4141
FSeqBootCommandline,
4242
)
4343
from Bio.Phylo.Applications import PhymlCommandline, RaxmlCommandline
44-
from ete3 import Tree
44+
from ete4 import Tree
4545
import numpy as np
4646

4747
from ..compara.ks import (
@@ -252,8 +252,8 @@ def smart_reroot(treefile, outgroupfile, outfile, format=0):
252252
Tree reading format options see here:
253253
http://packages.python.org/ete2/tutorial/tutorial_trees.html#reading-newick-trees
254254
"""
255-
tree = Tree(treefile, format=format)
256-
leaves = [t.name for t in tree.get_leaves()][::-1]
255+
tree = Tree(treefile, parser=format)
256+
leaves = [t.name for t in list(tree.leaves())][::-1]
257257
outgroup = []
258258
for o in must_open(outgroupfile):
259259
o = o.strip()
@@ -271,12 +271,12 @@ def smart_reroot(treefile, outgroupfile, outfile, format=0):
271271
return treefile
272272

273273
try:
274-
tree.set_outgroup(tree.get_common_ancestor(*outgroup))
274+
tree.set_outgroup(tree.common_ancestor(*outgroup))
275275
except ValueError:
276276
assert type(outgroup) == list
277277
outgroup = outgroup[0]
278278
tree.set_outgroup(outgroup)
279-
tree.write(outfile=outfile, format=format)
279+
tree.write(outfile=outfile, parser=format)
280280

281281
logger.debug("Rerooted tree printed to {0}".format(outfile))
282282
return outfile
@@ -374,9 +374,9 @@ def build_nj_phylip(alignment, outfile, outgroup, work_dir="."):
374374
nodesupport[node_children] = node.dist / 100.0
375375

376376
for k, v in nodesupport.items():
377-
ct_b.get_common_ancestor(*k).support = v
377+
ct_b.common_ancestor(*k).support = v
378378
print(ct_b)
379-
ct_b.write(format=0, outfile=outfile)
379+
ct_b.write(outfile=outfile, parser=0)
380380

381381
try:
382382
s = op.getsize(outfile)

src/jcvi/formats/cdt.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ def get_names(self):
4545

4646
def get_gtr_tree(self):
4747

48-
from ete3 import Tree
48+
from ete4 import Tree
4949

5050
fp = open(self.gtrfile)
5151
reader = csv.reader(fp, delimiter="\t")
@@ -68,7 +68,7 @@ def get_gtr_tree(self):
6868

6969
def print_newick(self, nwk_file):
7070

71-
self.gtr_tree.write(format=5, outfile=nwk_file)
71+
self.gtr_tree.write(outfile=nwk_file, parser=5)
7272
logger.debug("Newick tree written to `%s`", nwk_file)
7373

7474
def iter_partitions(self, cutoff=0.3, gtr=True):

src/jcvi/graphics/tree.py

Lines changed: 17 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
from itertools import groupby
66
import sys
77

8-
from ete3 import Tree
8+
from ete4 import Tree
99

1010
from ..apps.base import OptionParser, glob, logger
1111
from ..formats.base import LineFile
@@ -177,12 +177,20 @@ def draw_tree(
177177

178178
if reroot:
179179
if outgroup:
180-
R = t.get_common_ancestor(*outgroup)
180+
R = t.common_ancestor(*outgroup)
181181
else:
182182
# Calculate the midpoint node
183183
R = t.get_midpoint_outgroup()
184184

185185
if R is not t:
186+
# ete4 requires consistent support at root before rerooting
187+
root_children_support = [c.support for c in t.children]
188+
if None in root_children_support and any(
189+
s is not None for s in root_children_support
190+
):
191+
for child in t.children:
192+
if child.support is None:
193+
child.support = 1.0
186194
t.set_outgroup(R)
187195

188196
# By default, the distance to outgroup and non-outgroup is the same
@@ -192,7 +200,7 @@ def draw_tree(
192200
a, b = t.children
193201
# Avoid even split
194202
total = a.dist + b.dist
195-
newR = t.get_common_ancestor(*outgroup)
203+
newR = t.common_ancestor(*outgroup)
196204
a.dist = 0.9 * total
197205
b.dist = total - a.dist
198206

@@ -210,7 +218,7 @@ def rescale(dist):
210218
def rescale_divergence(divergence):
211219
return rescale(max_dist - divergence)
212220

213-
num_leaves = len(t.get_leaf_names())
221+
num_leaves = len(list(t.leaf_names()))
214222
yinterval = (1 - ystart) / num_leaves
215223
ytop = ystart + (num_leaves - 0.5) * yinterval
216224

@@ -228,10 +236,10 @@ def rescale_divergence(divergence):
228236
i = 0
229237
color_groups = [] # Used to plot groups to the right of the tree
230238
for n in t.traverse("postorder"):
231-
dist = n.get_distance(t)
239+
dist = t.get_distance(n, t)
232240
xx = rescale(dist)
233241

234-
if n.is_leaf():
242+
if n.is_leaf:
235243
yy = ystart + i * yinterval
236244
i += 1
237245

@@ -304,10 +312,10 @@ def rescale_divergence(divergence):
304312
alpha=0.4,
305313
lw=2,
306314
)
307-
support = n.support
315+
support = n.support if n.support is not None else 0
308316
if support > 1:
309317
support = support / 100.0
310-
if not n.is_root() and supportcolor:
318+
if not n.is_root and supportcolor:
311319
if support > scutoff / 100.0:
312320
ax.text(
313321
xx,
@@ -535,7 +543,7 @@ def repl(match):
535543
if repl.hpd:
536544
print(repl.hpd, file=sys.stderr)
537545

538-
return (Tree(treedata, format=1), repl.hpd) if changed else (Tree(treedata), None)
546+
return (Tree(treedata, parser=1), repl.hpd) if changed else (Tree(treedata), None)
539547

540548

541549
def main(args):

src/jcvi/utils/taxonomy.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@
3535

3636
from BeautifulSoup import BeautifulSoup
3737
from ClientForm import ParseResponse
38-
from ete3 import Tree
38+
from ete4 import Tree
3939

4040
from ..apps.base import ActionDispatcher, OptionParser, logger
4141

@@ -85,7 +85,7 @@ def __str__(self):
8585
return self.newick
8686

8787
def print_tree(self):
88-
t = Tree(self.newick, format=8)
88+
t = Tree(self.newick, parser=8)
8989
print(t)
9090

9191

@@ -122,9 +122,9 @@ def MRCA(list_of_taxids):
122122
"""
123123

124124
t = TaxIDTree(list_of_taxids)
125-
t = Tree(str(t), format=8)
125+
t = Tree(str(t), parser=8)
126126

127-
ancestor = t.get_common_ancestor(*t.get_leaves())
127+
ancestor = t.common_ancestor(*list(t.leaves()))
128128

129129
return ancestor.name
130130

tests/graphics/test_tree.py

Lines changed: 70 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,15 +3,81 @@
33

44
import os
55
import os.path as op
6+
import tempfile
67

8+
import matplotlib
79

8-
def disabled_test_tree_main():
10+
matplotlib.use("Agg")
11+
12+
import pytest
13+
14+
15+
def test_tree_main(tmp_path):
16+
"""Render the built-in demo tree to a PNG and check the file is created."""
917
from jcvi.apps.base import cleanup
1018
from jcvi.graphics.tree import main as tree_main
1119

12-
demo = "demo.png"
20+
demo = str(tmp_path / "demo.png")
1321
cleanup(demo)
1422

23+
os.chdir(tmp_path)
1524
tree_main(["demo", "--format", "png"])
16-
assert op.exists(demo)
17-
cleanup(demo)
25+
assert op.exists(str(tmp_path / "demo.png"))
26+
27+
28+
def test_tree_parse():
29+
"""Test that the ete4 Tree can be created from a newick string."""
30+
from ete4 import Tree
31+
32+
newick = "(((A:0.1,B:0.2)90:0.3,(C:0.15,D:0.25)80:0.2)100:0.05,E:0.4);"
33+
t = Tree(newick)
34+
assert t is not None
35+
leaves = list(t.leaf_names())
36+
assert sorted(leaves) == ["A", "B", "C", "D", "E"]
37+
38+
39+
def test_tree_parse_tree_function():
40+
"""Test parse_tree handles a plain newick file (no HPD)."""
41+
from jcvi.graphics.tree import parse_tree
42+
43+
newick = "(((A:0.1,B:0.2)90:0.3,(C:0.15,D:0.25)80:0.2)100:0.05,E:0.4);"
44+
with tempfile.NamedTemporaryFile(mode="w", suffix=".nwk", delete=False) as f:
45+
f.write(newick)
46+
fname = f.name
47+
try:
48+
t, hpd = parse_tree(fname)
49+
assert hpd is None
50+
leaves = list(t.leaf_names())
51+
assert sorted(leaves) == ["A", "B", "C", "D", "E"]
52+
finally:
53+
os.unlink(fname)
54+
55+
56+
def test_tree_truncate_name():
57+
"""Test the truncate_name helper function."""
58+
from jcvi.graphics.tree import truncate_name
59+
60+
assert truncate_name("ABCDEF", rule=None) == "ABCDEF"
61+
assert truncate_name("ABCDEF", rule="head3") == "DEF"
62+
assert truncate_name("ABCDEF", rule="ohead3") == "ABC"
63+
assert truncate_name("ABCDEF", rule="tail2") == "ABCD"
64+
assert truncate_name("ABCDEF", rule="otail2") == "EF"
65+
66+
67+
def test_tree_draw(tmp_path):
68+
"""Test that draw_tree runs without error on a simple tree."""
69+
import matplotlib.pyplot as plt
70+
from ete4 import Tree
71+
72+
from jcvi.graphics.tree import draw_tree
73+
74+
newick = "(((A:0.1,B:0.2)0.9:0.3,(C:0.15,D:0.25)0.8:0.2)1.0:0.05,E:0.4);"
75+
t = Tree(newick)
76+
77+
fig = plt.figure(figsize=(8, 6))
78+
ax = fig.add_axes([0, 0, 1, 1])
79+
draw_tree(ax, t)
80+
out = str(tmp_path / "tree_draw.png")
81+
fig.savefig(out)
82+
plt.close(fig)
83+
assert op.exists(out)

0 commit comments

Comments
 (0)