Skip to content

Commit 62e60bb

Browse files
authored
Merge pull request #27 from 23andMe/unrotate-newick
Unrotate Newick representation
2 parents 28211a6 + b5246ae commit 62e60bb

11 files changed

+156
-62
lines changed

.gitignore

-2
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@ __pycache__/
1212
.minio.sys
1313
.mypy_cache
1414
.pytest_cache
15-
.tox
1615

1716
# Distribution & packaging
1817
#----------------------------------
@@ -24,7 +23,6 @@ wheels/
2423
.eggs/
2524
*.egg
2625
*.egg-info/
27-
_version.py
2826

2927
# Editors & IDEs
3028
#----------------------------------

CHANGELOG.md

+26
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,32 @@
33
Format based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/)
44

55

6+
## [Unreleased]
7+
8+
No unreleased changes
9+
10+
[Unreleased]: https://github.com/23andMe/yhaplo/compare/2.1.6..HEAD
11+
12+
13+
## [2.1.6] - 2024-02-07
14+
15+
### Added
16+
- Python 3.12 support
17+
- `__str__` and `__repr__` methods
18+
- SNP-based haplogroup in MRCA output
19+
20+
### Changed
21+
- Newick representation is now unrotated by default
22+
23+
### Removed
24+
- `setuptools_scm`-generated version file
25+
26+
### Fixed
27+
- When generating Newick representation, recalculate maximum depth to support pruned trees
28+
29+
[2.1.6]: https://github.com/23andMe/yhaplo/compare/2.1.4..2.1.6
30+
31+
632
## [2.1.4] - 2024-01-29
733

834
### Added

README.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
# Yhaplo | Identifying Y-Chromosome Haplogroups
22

33
[![python](
4-
https://img.shields.io/badge/python-3.9%20%7C%203.10%20%7C%203.11-blue.svg)](
4+
https://img.shields.io/badge/python-3.9%20%7C%203.10%20%7C%203.11%20%7C%203.12-blue.svg)](
55
https://docs.python.org)
66
[![style](
77
https://img.shields.io/badge/style-black-blue.svg)](

pyproject.toml

+1-20
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,5 @@
11
[build-system]
2-
requires = [
3-
"setuptools>=61.0",
4-
"setuptools_scm[toml]>=6.2",
5-
]
2+
requires = ["setuptools>=64", "setuptools_scm>=8"]
63
build-backend = "setuptools.build_meta"
74

85
[project]
@@ -52,7 +49,6 @@ Repository = "https://github.com/23andMe/yhaplo.git"
5249
Changelog = "https://github.com/23andMe/yhaplo/blob/master/CHANGELOG.md"
5350

5451
[tool.setuptools_scm]
55-
write_to = "yhaplo/_version.py"
5652

5753
[tool.isort]
5854
profile = "black"
@@ -80,18 +76,3 @@ norecursedirs = [
8076
".*",
8177
"build",
8278
]
83-
84-
[tool.tox]
85-
legacy_tox_ini = """
86-
[tox]
87-
envlist = py39, py310, py311
88-
89-
[testenv]
90-
commands =
91-
pytest
92-
sitepackages = false
93-
deps =
94-
pytest
95-
extras =
96-
vcf
97-
"""

scripts/validate_yhaplo.sh

+1-1
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ yhaplo --example_text \
4040
--breadth_first \
4141
--depth_first \
4242
--depth_first_table \
43-
--mrca Q J \
43+
--mrca Q-M3 R-V88 \
4444
--snp_query L1335,S730,S530,foo
4545
echo -e "\n"
4646

yhaplo/config.py

+6-1
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
import numpy as np
1111
from numpy.typing import NDArray
1212

13-
from yhaplo._version import __version__
13+
from yhaplo import __version__
1414
from yhaplo.api.command_line_args import get_command_line_arg_defaults
1515
from yhaplo.utils.loaders import DataFile
1616

@@ -216,6 +216,11 @@ def __init__(
216216
if self.suppress_output:
217217
self.override_output_generating_args()
218218

219+
def __repr__(self) -> str:
220+
"""Return string representation."""
221+
222+
return f"<{__name__}.{self.__class__.__name__}: command_line_args={self.args}>"
223+
219224
def set_params_general(
220225
self,
221226
out_dir: Optional[str],

yhaplo/node.py

+60-23
Original file line numberDiff line numberDiff line change
@@ -100,9 +100,17 @@ def __init__(
100100

101101
# String representations
102102
# ----------------------------------------------------------------------
103-
def __str__(self) -> str:
103+
def __repr__(self) -> str:
104104
"""Return string representation."""
105105

106+
return (
107+
f"<{__name__}.{self.__class__.__name__}: "
108+
f'label="{self.label}", hg_snp="{self.hg_snp}">'
109+
)
110+
111+
def __str__(self) -> str:
112+
"""Return printable string representation."""
113+
106114
return self.str_simple
107115

108116
@property
@@ -516,14 +524,32 @@ def write_newick(
516524
use_hg_snp_label: bool = False,
517525
align_tips: bool = False,
518526
platform: Optional[str] = None,
527+
rotate: bool = False,
519528
) -> None:
520-
"""Write Newick string for the subtree rooted at this node."""
529+
"""Write Newick representation of the subtree rooted at this node.
530+
531+
Parameters
532+
----------
533+
newick_fp : str
534+
File path to which to write Newick representation.
535+
use_hg_snp_label : bool, optional
536+
Use SNP-based haplogroup labels rather than YCC haplogroup labels.
537+
align_tips : bool, optional
538+
When True, set branch lengths to align the tips of the tree.
539+
platform : str | None, optional
540+
23andMe platform to use for computing branch lengths.
541+
rotate : bool, optional
542+
Rotate nodes. By default, branches will be ordered top to bottom.
543+
Rotating nodes orders branches bottom to top, which is left to right
544+
when an image is rotated 90 degrees to the right.
521545
546+
"""
522547
if not type(self).config.suppress_output:
523548
newick = self.build_newick(
524549
use_hg_snp_label=use_hg_snp_label,
525550
align_tips=align_tips,
526551
platform=platform,
552+
rotate=rotate,
527553
)
528554
with open(newick_fp, "w") as out_file:
529555
out_file.write(newick + "\n")
@@ -550,6 +576,7 @@ def build_newick(
550576
use_hg_snp_label: bool = False,
551577
align_tips: bool = False,
552578
platform: Optional[str] = None,
579+
rotate: bool = False,
553580
) -> str:
554581
"""Build Newick string for the subtree rooted at this node.
555582
@@ -561,24 +588,25 @@ def build_newick(
561588
When True, set branch lengths to align the tips of the tree.
562589
platform : str | None, optional
563590
23andMe platform to use for computing branch lengths.
591+
rotate : bool, optional
592+
Rotate nodes. By default, branches will be ordered top to bottom.
593+
Rotating nodes orders branches bottom to top, which is left to right
594+
when an image is rotated 90 degrees to the right.
564595
565596
Returns
566597
-------
567598
newick : str
568599
Newick representation of the tree.
569600
570601
"""
571-
subtree_max_depth = (
572-
self.tree.max_depth
573-
if self.is_root
574-
else np.max([node.depth for node in self.iter_depth_first()])
575-
)
602+
subtree_max_depth = np.max([node.depth for node in self.iter_depth_first()])
576603
newick = (
577604
self.build_newick_recursive(
578605
use_hg_snp_label=use_hg_snp_label,
579606
align_tips=align_tips,
580607
subtree_max_depth=subtree_max_depth,
581608
platform=platform,
609+
rotate=rotate,
582610
)
583611
+ ";"
584612
)
@@ -591,6 +619,7 @@ def build_newick_recursive(
591619
align_tips: bool = False,
592620
subtree_max_depth: Optional[int] = None,
593621
platform: Optional[str] = None,
622+
rotate: bool = False,
594623
) -> str:
595624
"""Build Newick string recursively for the subtree rooted at this node.
596625
@@ -605,6 +634,10 @@ def build_newick_recursive(
605634
Default to maximum depth of full tree.
606635
platform : str | None, optional
607636
23andMe platform to use for computing branch lengths.
637+
rotate : bool, optional
638+
Rotate nodes. By default, branches will be ordered top to bottom.
639+
Rotating nodes orders branches bottom to top, which is left to right
640+
when an image is rotated 90 degrees to the right.
608641
609642
Returns
610643
-------
@@ -613,23 +646,27 @@ def build_newick_recursive(
613646
614647
"""
615648
subtree_max_depth = subtree_max_depth or type(self).tree.max_depth
616-
617-
if not self.is_leaf:
618-
child_string_list = []
619-
for child in self.child_list[::-1]:
620-
child_string = child.build_newick_recursive(
621-
use_hg_snp_label=use_hg_snp_label,
622-
align_tips=align_tips,
623-
subtree_max_depth=subtree_max_depth,
624-
platform=platform,
649+
child_list = self.child_list if not rotate else self.child_list[::-1]
650+
children_string = (
651+
(
652+
"("
653+
+ ",".join(
654+
[
655+
child.build_newick_recursive(
656+
use_hg_snp_label=use_hg_snp_label,
657+
align_tips=align_tips,
658+
subtree_max_depth=subtree_max_depth,
659+
platform=platform,
660+
rotate=rotate,
661+
)
662+
for child in child_list
663+
]
625664
)
626-
child_string_list.append(child_string)
627-
628-
children = ",".join(child_string_list)
629-
children_string = f"({children})"
630-
else:
631-
children_string = ""
632-
665+
+ ")"
666+
)
667+
if not self.is_leaf
668+
else ""
669+
)
633670
branch_label = self.hg_snp if use_hg_snp_label else self.label
634671
branch_length = self.get_branch_length(
635672
align_tips=align_tips,

yhaplo/path.py

+11-4
Original file line numberDiff line numberDiff line change
@@ -88,17 +88,24 @@ def copy_all_attributes_other_than_node(self, other: Path) -> None:
8888
self.num_anc_since_push_through = other.num_anc_since_push_through
8989
self.num_der_since_push_through = other.num_der_since_push_through
9090

91-
def __str__(self) -> str:
91+
def __repr__(self) -> str:
9292
"""Return string representation."""
9393

94-
str_ = (
94+
return (
95+
f"<{__name__}.{self.__class__.__name__}: "
96+
f"num_ancestral={self.num_ancestral}, num_derived={self.num_derived}, "
97+
f'node_string="{self.node_string}", snp_string="{self.snp_string}">'
98+
)
99+
100+
def __str__(self) -> str:
101+
"""Return printable string representation."""
102+
103+
return (
95104
f"{self.num_ancestral} {self.num_derived}\n"
96105
f"{self.node_string}\n"
97106
f"{self.snp_string}"
98107
)
99108

100-
return str_
101-
102109
# Properties
103110
# ----------------------------------------------------------------------
104111
@property

yhaplo/sample.py

+11-3
Original file line numberDiff line numberDiff line change
@@ -119,14 +119,22 @@ def __init__(self, iid: IID_TYPE):
119119

120120
type(self).sample_list.append(self)
121121

122-
def __str__(self) -> str:
122+
def __repr__(self) -> str:
123123
"""Return string representation."""
124124

125-
sample_string = (
125+
return (
126+
f"<{__name__}.{self.__class__.__name__}: "
127+
f'iid={str(self.iid)}, hg_snp_obs="{self.hg_snp_obs}", '
128+
f'hg_snp="{self.hg_snp}", haplogroup="{self.haplogroup}">'
129+
)
130+
131+
def __str__(self) -> str:
132+
"""Return printable string representation."""
133+
134+
return (
126135
f"{str(self.iid):8s} {self.hg_snp_obs:15s} "
127136
f"{self.hg_snp:15s} {self.haplogroup:25s}"
128137
)
129-
return sample_string
130138

131139
# Haplogroup calling
132140
# ----------------------------------------------------------------------

yhaplo/snp.py

+19-3
Original file line numberDiff line numberDiff line change
@@ -108,14 +108,22 @@ def set_label(self, label: str) -> None:
108108
) = parse_snp_label(label, Config.snp_label_letters_rank_dict)
109109
self.label_cleaned = clean_snp_label(label)
110110

111+
def __repr__(self) -> str:
112+
"""Return string representation."""
113+
114+
return (
115+
f"<{__name__}.{self.__class__.__name__}: "
116+
f'label="{self.label}", node.label="{self.node.label}", position={self.position}, '
117+
f'mutation="{self.ancestral}->{self.derived}">'
118+
)
119+
111120
def __str__(self) -> str:
112-
"""Return medium-length string representation."""
121+
"""Return printable string representation."""
113122

114-
str_ = (
123+
return (
115124
f"{self.label:15s} {self.node.label:25s} {self.position:8d} "
116125
f"{self.ancestral}->{self.derived}"
117126
)
118-
return str_
119127

120128
@property
121129
def str_with_all_names(self) -> str:
@@ -383,6 +391,14 @@ def __init__(
383391
self.haplogroup = haplogroup
384392
self.tree = tree
385393

394+
def __repr__(self) -> str:
395+
"""Return string representation."""
396+
397+
return (
398+
f"<{__name__}.{self.__class__.__name__}: "
399+
f'name="{self.name}", haplogroup="{self.haplogroup}">'
400+
)
401+
386402
def add_to_node(self) -> bool:
387403
"""Add this dropped marker to the corresponding node, if it exists."""
388404

0 commit comments

Comments
 (0)