Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/lints.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -20,4 +20,4 @@ jobs:
enable-cache: true

- name: nox (lints and style)
run: uv run --with nox nox -s lints style
run: uv run nox -s lints style
68 changes: 50 additions & 18 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,12 +1,18 @@
[build-system]
requires = ["hatchling"]
requires = [
"hatchling",
]
build-backend = "hatchling.build"

[project]
name = "paleomix"
dynamic = ["version"]
dynamic = [
"version",
]

authors = [{ name = "Mikkel Schubert", email = "MikkelSch@gmail.com" }]
authors = [
{ name = "Mikkel Schubert", email = "MikkelSch@gmail.com" },
]
license = { file = "LICENSE" }
description = "Bioinformatics pipelines for HTS data"
readme = { file = "README.rst", content-type = "text/x-rst" }
Expand All @@ -23,7 +29,12 @@ dependencies = [
"typing_extensions>=4.0",
]

keywords = ["pipeline", "bioinformatics", "hts", "bam"]
keywords = [
"pipeline",
"bioinformatics",
"hts",
"bam",
]
classifiers = [
"Development Status :: 5 - Production/Stable",
"Intended Audience :: Science/Research",
Expand Down Expand Up @@ -52,13 +63,13 @@ nox = [
]

testing = [
"coverage[toml]~=7.6",
"pytest~=8.3",
"pytest-cov~=6.0",
"coverage[toml]~=7.15",
"pytest~=9.1",
"pytest-cov~=7.1",
]

linting = [
"ruff==0.9.3",
"ruff==0.15.22",
]

typing = [
Expand Down Expand Up @@ -87,7 +98,7 @@ show_missing = true
precision = 1

exclude_also = [
# Exclude code-blocks that are only used during (pyright) type-checking
# Exclude code-blocks that are only used during type-checking
"^if TYPE_CHECKING:",
]

Expand All @@ -99,7 +110,12 @@ profile = "black"

[tool.pyright]
# Used since `strict = [...]` prevents individual `reportX` settings:
include = ["src", "tests", "typings", "noxfile.py"]
include = [
"src",
"tests",
"typings",
"noxfile.py",
]
# typeCheckingMode = "strict"

# Location of custom stub files
Expand All @@ -111,16 +127,22 @@ reportUnnecessaryIsInstance = "none"
reportUnusedCallResult = "none"

[tool.pytest.ini_options]
markers = ["slow"]
markers = [
"slow",
]

[tool.ruff]
target-version = "py39"
output-format = "concise"

exclude = ["docs/**"]
exclude = [
"docs/**",
]

[tool.ruff.lint]
select = ["ALL"]
select = [
"ALL",
]

ignore = [
# Rules either deemed too burdensome to implement or not useful
Expand Down Expand Up @@ -169,10 +191,20 @@ ignore = [
]

# flake8-annotations - pending rework using typesafe framework
"src/paleomix/pipelines/*/makefile.py" = ["ANN"]
"src/paleomix/pipelines/*/pipeline.py" = ["ANN"]
"src/paleomix/pipelines/*/project.py" = ["ANN"]
"src/paleomix/pipelines/zonkey/**.py" = ["ANN"]
"src/paleomix/pipelines/*/makefile.py" = [
"ANN",
]
"src/paleomix/pipelines/*/pipeline.py" = [
"ANN",
]
"src/paleomix/pipelines/*/project.py" = [
"ANN",
]
"src/paleomix/pipelines/zonkey/**.py" = [
"ANN",
]

[tool.uv]
reinstall-package = ["paleomix"]
reinstall-package = [
"paleomix",
]
2 changes: 1 addition & 1 deletion src/paleomix/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ def main(argv: list[str] | None = None) -> int:
if not argv or argv[0] in ("-h", "--help", "help"):
print(_HELP.format(version=paleomix.__version__))
return 0
elif argv[0] in ("--version",):
elif argv[0] == "--version":
print(f"paleomix v{paleomix.__version__}")
return 0

Expand Down
2 changes: 1 addition & 1 deletion src/paleomix/common/fileutils.py
Original file line number Diff line number Diff line change
Expand Up @@ -168,7 +168,7 @@ def open_rb(filename: PathTypes) -> IO[bytes]:
header = handle.peek(2)

if header.startswith(b"\x1f\x8b"):
return cast(IO[bytes], _GzipFile(mode="rb", fileobj=handle))
return cast("IO[bytes]", _GzipFile(mode="rb", fileobj=handle))
elif header.startswith(b"BZ"):
return _BZ2File(handle, "rb")
else:
Expand Down
5 changes: 5 additions & 0 deletions src/paleomix/common/formats/bed.py
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,11 @@ def __lt__(self, obj: object) -> bool:

return bed_1 < bed_2

def __hash__(self) -> int:
return hash(
(self.contig, self.start, self.end, self.name, self.score, self.strand)
)


def read_bed_file(
filename: str,
Expand Down
4 changes: 2 additions & 2 deletions src/paleomix/common/procs.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ def quote_args(args: object) -> str:
objects: Iterable[object]
if isinstance(args, os.PathLike):
# WORKAROUND for pyright warning about "partially unknown" types
objects = [cast(object, args)]
objects = [cast("object", args)]
elif isinstance(args, (str, bytes)) or not isinstance(args, Iterable):
objects = [args]
else:
Expand All @@ -38,7 +38,7 @@ def quote_args(args: object) -> str:
values: list[str] = []
for value in objects:
if isinstance(value, os.PathLike):
value = os.fsdecode(cast(Any, value))
value = os.fsdecode(cast("Any", value))

if isinstance(value, bytes):
value = value.decode("utf-8", errors="replace")
Expand Down
3 changes: 3 additions & 0 deletions src/paleomix/common/utilities.py
Original file line number Diff line number Diff line change
Expand Up @@ -177,3 +177,6 @@ def __gt__(self, other: object) -> bool:
if not isinstance(other, type(self)):
return NotImplemented
return other < self

def __hash__(self) -> int:
raise NotImplementedError
2 changes: 1 addition & 1 deletion src/paleomix/common/versions.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ def __init__(
def call(self) -> tuple[str, ...]:
call = self._call
if call[0] == "%(PYTHON)s":
return (sys.executable,) + call[1:]
return (sys.executable, *call[1:])

return call

Expand Down
2 changes: 1 addition & 1 deletion src/paleomix/pipelines/bam/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ def main(argv: list[str], pipeline: str = "bam") -> int:
args = parser.parse_args(argv)
if args.command in ("new", "makefile", "mkfile"):
return _main_template()
elif args.command in ("example",):
elif args.command == "example":
return _main_copy_example(args.destination)

if args.command.startswith("dry") and args.pipeline_mode == "run":
Expand Down
2 changes: 1 addition & 1 deletion src/paleomix/pipelines/bam/makefile.py
Original file line number Diff line number Diff line change
Expand Up @@ -629,7 +629,7 @@ def _validate_prefixes(makefiles, pipeline_variant):
except (OSError, ValueError) as error:
if pipeline_variant == "bam":
raise MakefileError(f"Error reading/indexing FASTA: {error}")
logging.warning("Error reading/indexing FASTA: %s", error)
logger.warning("Error reading/indexing FASTA: %s", error)
else:
if max(contigs.values()) > _BAM_MAX_SEQUENCE_LENGTH:
logger.warning(
Expand Down
2 changes: 1 addition & 1 deletion src/paleomix/pipelines/bam/pipeline.py
Original file line number Diff line number Diff line change
Expand Up @@ -528,7 +528,7 @@ def run_mapdamage(layout: Layout, genome, records):
dependencies=(extra_task,),
)

if run_type in ("rescale",):
if run_type == "rescale":
# Rescales BAM quality scores using model built above
assert extra_task is not None
task = MapDamageRescaleNode(
Expand Down
6 changes: 2 additions & 4 deletions src/paleomix/pipelines/zonkey/build_mito.py
Original file line number Diff line number Diff line change
Expand Up @@ -173,17 +173,15 @@ def main(argv):
with open(args.output_prefix + ".summary", "w") as handle:
stats["filename"] = os.path.abspath(args.bam)

for key, value in sorted(stats.items()):
handle.write(f"{key}: {value}\n")
handle.writelines(f"{key}: {value}\n" for key, value in sorted(stats.items()))

with open(args.output_prefix + ".phy", "w") as handle:
handle.write(interleaved_phy(sequences_to_msa(sequences)))

with open(args.output_prefix + ".fasta", "w") as handle:
for key, record in sorted(sequences.items()):
handle.write(f">{key}\n")
for line in fragment(60, record.sequence):
handle.write(f"{line}\n")
handle.writelines(f"{line}\n" for line in fragment(60, record.sequence))

return 0

Expand Down
6 changes: 4 additions & 2 deletions src/paleomix/pipelines/zonkey/build_tped.py
Original file line number Diff line number Diff line change
Expand Up @@ -227,8 +227,10 @@ def write_summary(args, filename, statistics):
handle.write(f"name: {args.name}\n")
handle.write(f"filename: {os.path.abspath(args.bam)}\n")

for key in ("n_reads", "n_reads_used", "n_sites_incl_ts", "n_sites_excl_ts"):
handle.write("{}: {}\n".format(key, statistics.get(key, "MISSING")))
handle.writelines(
"{}: {}\n".format(key, statistics.get(key, "MISSING"))
for key in ("n_reads", "n_reads_used", "n_sites_incl_ts", "n_sites_excl_ts")
)


def process_bam(args, data, bam_handle, mapping):
Expand Down
9 changes: 3 additions & 6 deletions src/paleomix/pipelines/zonkey/parts/nuclear.py
Original file line number Diff line number Diff line change
Expand Up @@ -390,8 +390,7 @@ def _setup(self, temp: fileutils.PathTypes) -> None:
samples = [line.split(None, 1)[0] for line in in_handle]

with open(os.path.join(temp, "samples.clust"), "w") as handle:
for sample in samples:
handle.write(f"{sample} {sample} {sample}\n")
handle.writelines(f"{sample} {sample} {sample}\n" for sample in samples)

def _teardown(self, temp: fileutils.PathTypes) -> None:
for ext in ("log", "nosex"):
Expand Down Expand Up @@ -524,9 +523,7 @@ def _setup(self, temp: fileutils.PathTypes) -> None:
if self._k_file is not None:
stats = read_summary(self._k_file)
n_sites = float(stats[self._k_field])
k = max(
1, int(math.ceil(self._snp_distance / (self._genome_size / n_sites)))
)
k = max(1, math.ceil(self._snp_distance / (self._genome_size / n_sites)))

self._param_k = k
assert isinstance(self._command, AtomicCmd)
Expand Down Expand Up @@ -758,7 +755,7 @@ def _setup(self, temp: fileutils.PathTypes) -> None:
if not line:
continue

name, size, hits, _ = line.split("\t")
name, _, hits, _ = line.split("\t")
name = self._mapping.get(name, name)
if name not in self._contigs:
# Excluding contigs is allowed
Expand Down
2 changes: 1 addition & 1 deletion src/paleomix/tools/rmdup_collapsed.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ def copy_number(read: AlignedSegment) -> int:
if read.has_tag("XP"):
value, tag_type = read.get_tag("XP", with_value_type=True)
if tag_type == "i":
return cast(int, value)
return cast("int", value)

return 0

Expand Down
2 changes: 1 addition & 1 deletion tests/common_tests/command_test/command_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ def test_atomicfile__valid_paths() -> None:
def test_atomiccmd__paths__invalid_values(
cls: type[IOFileTypes], value: object
) -> None:
with pytest.raises(TypeError, match="expected str, bytes or os.PathLike object"):
with pytest.raises(TypeError, match=r"expected str, bytes or os.PathLike object"):
cls(value) # pyright: ignore[reportArgumentType]


Expand Down
9 changes: 6 additions & 3 deletions tests/common_tests/makefile_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -1005,17 +1005,20 @@ def test_fastq_path__path_must_meet_spec() -> None:
assert accept_any(_DUMMY_PATH, se_reads) == se_reads
assert accept_any(_DUMMY_PATH, pe_reads) == pe_reads

with pytest.raises(MakefileError, match="Expected value: a path without a {pair}"):
with pytest.raises(MakefileError, match=r"Expected value: a path without a {pair}"):
accept_se(_DUMMY_PATH, pe_reads)

with pytest.raises(MakefileError, match="Expected value: a path with a {pair} key"):
with pytest.raises(
MakefileError,
match=r"Expected value: a path with a {pair} key",
):
accept_pe(_DUMMY_PATH, se_reads)


@pytest.mark.parametrize("value", _common_invalid_values(extra=(1,)))
def test_fastq_path__rejects_non_strings(value: object) -> None:
spec = FASTQPath()
with pytest.raises(MakefileError, match="Expected value: a path without a {pair}"):
with pytest.raises(MakefileError, match=r"Expected value: a path without a {pair}"):
spec(_DUMMY_PATH, value)


Expand Down
4 changes: 2 additions & 2 deletions tests/common_tests/sequences_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
_REF_DST = "TGCAKYWSRMBDHVNX"


@pytest.mark.parametrize(("src", "dst"), zip(_REF_SRC, _REF_DST))
@pytest.mark.parametrize(("src", "dst"), tuple(zip(_REF_SRC, _REF_DST)))
def test_complement__single_nt(src: str, dst: str) -> None:
assert complement(src) == dst
assert complement(src.lower()) == dst.lower()
Expand Down Expand Up @@ -72,7 +72,7 @@ def test_reverse_complement() -> None:
_IUB_DST = "ACGTMRWSYKVHDB"


@pytest.mark.parametrize(("src", "dst"), zip(_IUB_SRC, _IUB_DST))
@pytest.mark.parametrize(("src", "dst"), tuple(zip(_IUB_SRC, _IUB_DST)))
def test_genotype__permutations(src: str, dst: str) -> None:
for seq in itertools.permutations(src):
assert encode_genotype("".join(seq)) == dst
Expand Down
2 changes: 1 addition & 1 deletion tests/common_tests/versions_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,7 @@ def test_requirement__version__command_not_found() -> None:
def test_requirement__version__command_not_executable() -> None:
obj = Requirement(call=("./README.rst",), regexp=r"v(\d+\.\d+)")

with pytest.raises(RequirementError, match="[Errno 13]"):
with pytest.raises(RequirementError, match=r"[Errno 13]"):
obj.version()


Expand Down
7 changes: 5 additions & 2 deletions tests/pipelines/bam/makefile_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -184,7 +184,7 @@ def test_genomes__simple_genome(tmp_path: Path) -> None:
def test_genomes__bad_wildcard_in_name(tmp_path: Path) -> None:
filepath = _write_genome_yaml(tmp_path, {"T*est": "/path/to/genome.fasta"})

with pytest.raises(MakefileError, match="'*' is not allowed in Genome names"):
with pytest.raises(MakefileError, match=r"'*' is not allowed in Genome names"):
_read_makefile(filepath)


Expand Down Expand Up @@ -727,5 +727,8 @@ def test_makefile__validation__paired_must_have_key(tmp_path: Path) -> None:
""",
)

with pytest.raises(MakefileError, match="Expected value: a path with a {pair} key"):
with pytest.raises(
MakefileError,
match=r"Expected value: a path with a {pair} key",
):
_read_makefile(filepath)
Loading
Loading