Skip to content

Commit 28a7e67

Browse files
mstoclaude
andcommitted
feat: forward delimiter parameters from copy_umi_from_read_name
Expose read_name_delimiter and umi_delimiter on copy_umi_from_read_name and forward them to extract_umis_from_read_name, so callers working with non-Illumina read-name conventions can use the higher-level helper. The remove_umi branch now uses the passed-in read_name_delimiter rather than the hardcoded Illumina constant. Defaults match the existing Illumina constants, so all existing callers are unaffected. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent aa8eb68 commit 28a7e67

2 files changed

Lines changed: 39 additions & 3 deletions

File tree

fgpyo/platform/illumina.py

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -87,17 +87,32 @@ def extract_umis_from_read_name(
8787

8888

8989
def copy_umi_from_read_name(
90-
rec: AlignedSegment, strict: bool = False, remove_umi: bool = False
90+
rec: AlignedSegment,
91+
strict: bool = False,
92+
remove_umi: bool = False,
93+
read_name_delimiter: str = _ILLUMINA_READ_NAME_DELIMITER,
94+
umi_delimiter: str = _ILLUMINA_UMI_DELIMITER,
9195
) -> bool:
9296
"""
9397
Copy a UMI from an alignment's read name to its `RX` SAM tag.
9498
9599
The UMI will not be copied to RX tag if it is invalid.
96100
101+
`strict`, `read_name_delimiter`, and `umi_delimiter` are forwarded to
102+
[`extract_umis_from_read_name`][fgpyo.platform.illumina.extract_umis_from_read_name] — see
103+
that function for their semantics. If you change the signature of either function, update
104+
the other to keep them in sync.
105+
97106
Args:
98107
rec: The alignment record to update.
99-
strict: If `True` and UMI invalid, will throw an exception
108+
strict: If `True` and UMI invalid, will throw an exception.
109+
Forwarded to `extract_umis_from_read_name`.
100110
remove_umi: If `True`, the UMI will be removed from the read name after copying.
111+
read_name_delimiter: The delimiter separating the components of the read name.
112+
Forwarded to `extract_umis_from_read_name`, and also used to strip the UMI
113+
segment when `remove_umi` is `True`.
114+
umi_delimiter: The delimiter separating multiple UMIs.
115+
Forwarded to `extract_umis_from_read_name`.
101116
102117
Returns:
103118
`True` if the UMI was successfully extracted, False if otherwise.
@@ -111,13 +126,15 @@ def copy_umi_from_read_name(
111126
umi = extract_umis_from_read_name(
112127
read_name=rec.query_name,
113128
strict=strict,
129+
read_name_delimiter=read_name_delimiter,
130+
umi_delimiter=umi_delimiter,
114131
)
115132
if umi is not None:
116133
if rec.has_tag("RX"):
117134
raise ValueError(f"Record {rec.query_name} already has a populated RX tag")
118135
rec.set_tag(tag="RX", value=umi)
119136
if remove_umi:
120-
last_index = rec.query_name.rfind(_ILLUMINA_READ_NAME_DELIMITER)
137+
last_index = rec.query_name.rfind(read_name_delimiter)
121138
rec.query_name = rec.query_name[:last_index] if last_index != -1 else rec.query_name
122139
return True
123140
elif strict:

tests/fgpyo/platform/test_illumina.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,25 @@ def test_copy_invalid_multi_umi_non_strict_returns_false() -> None:
149149
assert read.has_tag("RX") is False
150150

151151

152+
def test_copy_umi_from_read_name_custom_read_name_delimiter() -> None:
153+
"""Test that a custom read_name_delimiter is used for both extraction and UMI removal."""
154+
builder = SamBuilder()
155+
read = builder.add_single(name="abc_def_ghi_AAAA+TTTT")
156+
assert (
157+
copy_umi_from_read_name(read, remove_umi=True, read_name_delimiter="_") is True
158+
)
159+
assert read.get_tag("RX") == "AAAA-TTTT"
160+
assert read.query_name == "abc_def_ghi"
161+
162+
163+
def test_copy_umi_from_read_name_custom_umi_delimiter() -> None:
164+
"""Test that a custom umi_delimiter is forwarded when parsing multi-UMI segments."""
165+
builder = SamBuilder()
166+
read = builder.add_single(name="abc:def:ghi:AAAA|TTTT")
167+
assert copy_umi_from_read_name(read, umi_delimiter="|") is True
168+
assert read.get_tag("RX") == "AAAA-TTTT"
169+
170+
152171
def test_populated_rx_tag_raises() -> None:
153172
"""Test that we raise a ValueError when a record already has a populated RX tag."""
154173
builder = SamBuilder()

0 commit comments

Comments
 (0)