Skip to content

Commit 8b76f2b

Browse files
mstoclaude
andcommitted
chore: migrate type annotations to PEP 585 built-in generics
Closes #298. Follow-up to #292 (PEP 604). Now that fgpyo requires Python 3.10+ (#261), annotation-site uses of typing.List/Dict/Tuple/ Set/FrozenSet/Type are migrated to the PEP 585 built-ins (list, dict, tuple, set, frozenset, type). Applied via `ruff check --select UP006,UP035 --fix --unsafe-fixes` followed by `--select F401 --fix` to drop now-unused legacy imports. Two spots needed manual follow-up where the bare `Type` alias was equivalent to `Type[Any]` but `type` (the builtin) is stricter: - `fgpyo/util/inspect.py:311` — `cls: type` → `cls: type[Any]` to let mypy accept `cls._parsers()` on Metric subclasses. - `tests/fgpyo/util/test_metric.py:38` — `TypeVar("T", bound=type)` → `bound=type[Any]` to keep the decorator applicable to any class, not just metaclasses. No runtime behavior changes. `NamedTuple`, `TypeVar`, `TypeGuard`, `TypeAlias` are left as-is — no PEP 585 equivalent. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent d49cf36 commit 8b76f2b

31 files changed

Lines changed: 263 additions & 330 deletions

fgpyo/_requirements.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
"""Enforce requirements."""
22

3-
from typing import Callable
3+
from collections.abc import Callable
44

55

66
class RequirementError(Exception):

fgpyo/collections/__init__.py

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -78,14 +78,13 @@
7878
iterable objects as well as iterators.
7979
"""
8080

81+
from collections.abc import Callable
82+
from collections.abc import Iterable
83+
from collections.abc import Iterator
8184
from itertools import pairwise
8285
from operator import le
8386
from typing import Any
84-
from typing import Callable
8587
from typing import Generic
86-
from typing import Iterable
87-
from typing import Iterator
88-
from typing import List
8988
from typing import Protocol
9089
from typing import TypeVar
9190

@@ -144,7 +143,7 @@ def peek(self) -> IterType:
144143
else:
145144
raise StopIteration
146145

147-
def takewhile(self, pred: Callable[[IterType], bool]) -> List[IterType]:
146+
def takewhile(self, pred: Callable[[IterType], bool]) -> list[IterType]:
148147
"""
149148
Consumes from the iterator while pred is true, and returns the result as a List.
150149
@@ -159,7 +158,7 @@ def takewhile(self, pred: Callable[[IterType], bool]) -> List[IterType]:
159158
List[V]: A list of the values from the iterator, in order, up until and excluding
160159
the first value that does not match the predicate.
161160
"""
162-
xs: List[IterType] = []
161+
xs: list[IterType] = []
163162
while self.can_peek() and pred(self._peek):
164163
xs.append(next(self))
165164
return xs

fgpyo/fasta/builder.py

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -47,12 +47,11 @@
4747
"""
4848

4949
import textwrap
50+
from collections.abc import Iterator
5051
from contextlib import contextmanager
5152
from pathlib import Path
5253
from typing import TYPE_CHECKING
5354
from typing import Any
54-
from typing import Dict
55-
from typing import Iterator
5655

5756
from pysam import FastaFile
5857

@@ -179,7 +178,7 @@ def __init__(
179178
self.assembly: str = assembly
180179
self.species: str = species
181180
self.line_length: int = line_length
182-
self.__contig_builders: Dict[str, ContigBuilder] = {}
181+
self.__contig_builders: dict[str, ContigBuilder] = {}
183182

184183
def __getitem__(self, key: str) -> ContigBuilder:
185184
"""Access instance of ContigBuilder by name."""

fgpyo/fasta/sequence_dictionary.py

Lines changed: 20 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -129,18 +129,16 @@
129129
import itertools
130130
import re
131131
import sys
132+
from collections.abc import Iterator
133+
from collections.abc import Mapping
134+
from collections.abc import MutableMapping
132135
from dataclasses import dataclass
133136
from dataclasses import field
134137
from dataclasses import replace
135138
from enum import unique
136139
from pathlib import Path
140+
from re import Pattern
137141
from typing import Any
138-
from typing import Dict
139-
from typing import Iterator
140-
from typing import List
141-
from typing import Mapping
142-
from typing import MutableMapping
143-
from typing import Pattern
144142
from typing import overload
145143

146144
from fgpyo import sam
@@ -177,7 +175,7 @@ class Keys(StrEnum):
177175
URI = "UR"
178176

179177
@staticmethod
180-
def attributes() -> List[str]:
178+
def attributes() -> list[str]:
181179
"""
182180
The list of keys that are allowed to be attributes in `SequenceMetadata`.
183181
@@ -253,7 +251,7 @@ class SequenceMetadata(MutableMapping[Keys | str, str]):
253251
name: str
254252
length: int
255253
index: int
256-
attributes: Dict[Keys | str, str] = field(default_factory=dict)
254+
attributes: dict[Keys | str, str] = field(default_factory=dict)
257255

258256
def __post_init__(self) -> None:
259257
"""Any post initialization validation should go here."""
@@ -267,13 +265,13 @@ def __post_init__(self) -> None:
267265
raise ValueError(f"'{Keys.SEQUENCE_LENGTH}' should not given in the list of attributes")
268266

269267
@property
270-
def aliases(self) -> List[str]:
268+
def aliases(self) -> list[str]:
271269
"""The aliases (not including the primary) name."""
272270
aliases = self.attributes.get(Keys.ALIASES)
273271
return [] if aliases is None else aliases.split(",")
274272

275273
@property
276-
def all_names(self) -> List[str]:
274+
def all_names(self) -> list[str]:
277275
"""A list of all names, including the primary name and aliases, in that order."""
278276
return [self.name] + self.aliases
279277

@@ -344,14 +342,14 @@ def same_as(self, other: "SequenceMetadata") -> bool:
344342
else:
345343
return self_m5 == other_m5
346344

347-
def to_sam(self) -> Dict[str, Any]:
345+
def to_sam(self) -> dict[str, Any]:
348346
"""
349347
Converts the sequence metadata to a SAM-formatted dictionary.
350348
351349
Equivalent to one item in the list of sequences from
352350
`pysam.AlignmentHeader#to_dict()["SQ"]`.
353351
"""
354-
meta_dict: Dict[str, Any] = {
352+
meta_dict: dict[str, Any] = {
355353
f"{Keys.SEQUENCE_NAME}": self.name,
356354
f"{Keys.SEQUENCE_LENGTH}": self.length,
357355
}
@@ -361,7 +359,7 @@ def to_sam(self) -> Dict[str, Any]:
361359
return meta_dict
362360

363361
@staticmethod
364-
def from_sam(meta: Dict[Keys | str, Any], index: int) -> "SequenceMetadata":
362+
def from_sam(meta: dict[Keys | str, Any], index: int) -> "SequenceMetadata":
365363
"""
366364
Builds a `SequenceMetadata` from a dictionary.
367365
@@ -436,13 +434,13 @@ class SequenceDictionary(Mapping[str | int, SequenceMetadata]):
436434
infos: the ordered collection of sequence metadata
437435
"""
438436

439-
infos: List[SequenceMetadata]
440-
_dict: Dict[str, SequenceMetadata] = field(init=False, repr=False)
437+
infos: list[SequenceMetadata]
438+
_dict: dict[str, SequenceMetadata] = field(init=False, repr=False)
441439

442440
def __post_init__(self) -> None:
443441
"""Builds the internal name-to-metadata lookup dictionary."""
444442
# Initialize a mapping from sequence name to the sequence metadata for all names
445-
self_dict: Dict[str, SequenceMetadata] = {}
443+
self_dict: dict[str, SequenceMetadata] = {}
446444
for index, info in enumerate(self.infos):
447445
if info.index != index:
448446
raise ValueError(
@@ -466,13 +464,13 @@ def same_as(self, other: "SequenceDictionary") -> bool:
466464
return False
467465
return all(this.same_as(that) for this, that in zip(self.infos, other.infos, strict=True))
468466

469-
def to_sam(self) -> List[Dict[str, Any]]:
467+
def to_sam(self) -> list[dict[str, Any]]:
470468
"""Converts the list of dictionaries, one per sequence."""
471469
return [meta.to_sam() for meta in self.infos]
472470

473471
def to_sam_header(
474472
self,
475-
extra_header: Dict[str, Any] | None = None,
473+
extra_header: dict[str, Any] | None = None,
476474
) -> pysam.AlignmentHeader:
477475
"""
478476
Converts the sequence dictionary to a `pysam.AlignmentHeader`.
@@ -481,7 +479,7 @@ def to_sam_header(
481479
extra_header: a dictionary of extra values to add to the header, None otherwise. See
482480
`:~pysam.AlignmentHeader` for more details.
483481
"""
484-
header_dict: Dict[str, Any] = {
482+
header_dict: dict[str, Any] = {
485483
"HD": {"VN": "1.5"},
486484
"SQ": self.to_sam(),
487485
}
@@ -503,11 +501,11 @@ def from_sam(data: pysam.AlignmentHeader) -> "SequenceDictionary": ...
503501

504502
@staticmethod
505503
@overload
506-
def from_sam(data: List[Dict[str, Any]]) -> "SequenceDictionary": ...
504+
def from_sam(data: list[dict[str, Any]]) -> "SequenceDictionary": ...
507505

508506
@staticmethod
509507
def from_sam(
510-
data: Path | pysam.AlignmentFile | pysam.AlignmentHeader | List[Dict[str, Any]],
508+
data: Path | pysam.AlignmentFile | pysam.AlignmentHeader | list[dict[str, Any]],
511509
) -> "SequenceDictionary":
512510
"""
513511
Creates a `SequenceDictionary` from a SAM file or its header.
@@ -531,7 +529,7 @@ def from_sam(
531529
seq_dict = SequenceDictionary.from_sam(fh.header)
532530
else: # assuming `data` is a `list[dict[str, Any]]`
533531
try:
534-
infos: List[SequenceMetadata] = [
532+
infos: list[SequenceMetadata] = [
535533
SequenceMetadata.from_sam(meta=meta, index=index)
536534
for index, meta in enumerate(data)
537535
]

fgpyo/fastx/__init__.py

Lines changed: 8 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -26,21 +26,18 @@
2626
2727
"""
2828

29+
from collections.abc import Iterator
2930
from contextlib import AbstractContextManager
3031
from pathlib import Path
3132
from types import TracebackType
32-
from typing import Iterator
33-
from typing import List
34-
from typing import Tuple
35-
from typing import Type
3633

3734
from pysam import FastxFile
3835
from pysam import FastxRecord
3936

4037
from fgpyo.util.types import all_not_none
4138

4239

43-
class FastxZipped(AbstractContextManager, Iterator[Tuple[FastxRecord, ...]]):
40+
class FastxZipped(AbstractContextManager, Iterator[tuple[FastxRecord, ...]]):
4441
"""
4542
A context manager that will lazily zip over any number of FASTA/FASTQ files.
4643
@@ -55,22 +52,22 @@ def __init__(self, *paths: Path | str, persist: bool = False) -> None:
5552
if len(paths) <= 0:
5653
raise ValueError(f"Must provide at least one FASTX to {self.__class__.__name__}")
5754
self._persist: bool = persist
58-
self._paths: Tuple[Path | str, ...] = paths
55+
self._paths: tuple[Path | str, ...] = paths
5956
self._fastx = tuple(FastxFile(str(path), persist=self._persist) for path in self._paths)
6057

6158
@staticmethod
6259
def _name_minus_ordinal(name: str) -> str:
6360
"""Return the name of the FASTX record minus its ordinal suffix (e.g. "/1" or "/2")."""
6461
return name[: len(name) - 2] if len(name) >= 2 and name[-2] == "/" else name
6562

66-
def __next__(self) -> Tuple[FastxRecord, ...]:
63+
def __next__(self) -> tuple[FastxRecord, ...]:
6764
"""Return the next set of FASTX records from the zipped FASTX files."""
6865
records = tuple(next(handle, None) for handle in self._fastx)
6966

7067
if all(record is None for record in records):
7168
raise StopIteration
7269
elif not all_not_none(records):
73-
non_none_names: List[str | None] = [
70+
non_none_names: list[str | None] = [
7471
record.name for record in records if record is not None
7572
]
7673
assert all_not_none(non_none_names) # type narrowing
@@ -86,17 +83,17 @@ def __next__(self) -> Tuple[FastxRecord, ...]:
8683
)
8784
)
8885

89-
names_with_ordinals: List[str | None] = [record.name for record in records]
86+
names_with_ordinals: list[str | None] = [record.name for record in records]
9087
assert all_not_none(names_with_ordinals) # type narrowing
91-
record_names: List[str] = [self._name_minus_ordinal(name) for name in names_with_ordinals]
88+
record_names: list[str] = [self._name_minus_ordinal(name) for name in names_with_ordinals]
9289
if len(set(record_names)) != 1:
9390
raise ValueError(f"FASTX record names do not all match, found: {record_names}")
9491

9592
return records
9693

9794
def __exit__(
9895
self,
99-
exc_type: Type[BaseException] | None,
96+
exc_type: type[BaseException] | None,
10097
exc_val: BaseException | None,
10198
exc_tb: TracebackType | None,
10299
) -> bool | None:

fgpyo/io/__init__.py

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -63,21 +63,20 @@
6363
import os
6464
import sys
6565
import warnings
66+
from collections.abc import Generator
67+
from collections.abc import Iterable
68+
from collections.abc import Iterator
6669
from contextlib import contextmanager
6770
from io import TextIOWrapper
6871
from pathlib import Path
6972
from typing import IO
7073
from typing import Any
71-
from typing import Generator
72-
from typing import Iterable
73-
from typing import Iterator
74-
from typing import Set
7574
from typing import cast
7675

7776
from zlib_ng import gzip_ng
7877
from zlib_ng import gzip_ng_threaded
7978

80-
COMPRESSED_FILE_EXTENSIONS: Set[str] = {".gz", ".bgz"}
79+
COMPRESSED_FILE_EXTENSIONS: set[str] = {".gz", ".bgz"}
8180

8281

8382
def assert_path_is_readable(path: Path) -> None:

fgpyo/platform/illumina.py

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,15 +9,13 @@
99
1010
"""
1111

12-
from typing import Set
13-
1412
from pysam import AlignedSegment
1513

1614
SAM_UMI_DELIMITER: str = "-"
1715
"""Multiple UMI delimiter, which SAM specification recommends should be a hyphen;
1816
see specification here: https://samtools.github.io/hts-specs/SAMtags.pdf"""
1917

20-
_VALID_UMI_CHARACTERS: Set[str] = set("ACGTN")
18+
_VALID_UMI_CHARACTERS: set[str] = set("ACGTN")
2119
"""Illumina's restricted UMI characters;
2220
https://support.illumina.com/help/BaseSpace_Sequence_Hub_OLH_009008_2/Source/Informatics/BS/FileFormat_FASTQ-files_swBS.htm.""" # noqa
2321

0 commit comments

Comments
 (0)