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
24 changes: 6 additions & 18 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -89,26 +89,14 @@ uv run poe fix-and-check-all

1. Clone the repository recursively, ensure you are on the main branch, and that the working directory is clean.
2. Check out a new branch to prepare the library for release.
3. Bump the version of the library to the desired SemVer using [`uv run hatch version`](https://hatch.pypa.io/latest/version/#updating).

`hatch version` supports updating to a specific version number.
For example, to update the project version to `0.2.0`:
```console
uv run hatch version "0.2.0"

```
`hatch version` also supports incrementing the major, minor, or patch segment.
For example, to bump the minor version segment:
```console
uv run hatch version minor
```
5. Commit the version bump changes with a Git commit message like `chore(release): bump to #.#.#`.
6. Push the commit, open a PR, ensure tests pass, and seek reviews.
7. Squash merge the PR into the `main` branch.
8. Tag the new commit on the main branch with the bumped version number.
3. Bump the version of the library to the desired SemVer using [`uv version`](https://docs.astral.sh/uv/reference/cli/#uv-version).
4. Commit the version bump changes with a Git commit message like `chore(release): bump to #.#.#`.
5. Push the commit, open a PR, ensure tests pass, and seek reviews.
6. Squash merge the PR into the `main` branch.
7. Tag the new commit on the main branch with the bumped version number.

> [!WARNING]
> The tag **must** be a valid SemVer version number and **must** match the version set by `uv run hatch version` in (3). The [publishing GitHub Action](.github/workflows/publish_syncup) is activated by a new tag on the `main` branch containing a valid SemVer version.
> The tag **must** be a valid SemVer version number and **must** match the version set by `uv version` in (3). The [publishing GitHub Action](.github/workflows/publish_syncup) is activated by a new tag on the `main` branch containing a valid SemVer version.

GitHub Actions will take care of the remainder of the deployment and release process:

Expand Down
2 changes: 1 addition & 1 deletion LICENSE
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
MIT License

Copyright © 2025 Fulcrum Genomics LLC
Copyright © 2025 Clint Valentine

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
Expand Down
32 changes: 21 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,20 +9,30 @@

Synchronizers for lazy iterators.

## Recommended Installation

Install the Python package and dependency management tool [`uv`](https://docs.astral.sh/uv/getting-started/installation/) using official documentation.

Install the dependencies of the project with:

```console
uv sync --locked
pip install syncup
```

To check successful installation, run:

```console
uv run syncup --help
## Example

Iterate though a complete FASTX file and a filtered FASTX file, synchronizing on records present in both:

```python
with (
FastxFile(in_source) as source,
FastxFile(in_subseq) as subseq,
):
for count, (fq, fq_subseq) in enumerate(
sync(
iter1=source,
iter2=subseq,
key1=lambda rec: rec.name,
key2=lambda rec: rec.name,
cmp_func=lambda x, y: (x == y) - 1, # only advance iter1 when non-equal
),
start=1,
):
assert fq.name == fq_subseq.name, f"Names for record {count} should be equal!"
```

## Development and Testing
Expand Down
90 changes: 38 additions & 52 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,20 +1,17 @@
[build-system]
requires = ["hatchling"]
requires = ["hatchling"]
build-backend = "hatchling.build"

[project]
name = "syncup"
version = "0.1.0"
description = "Synchronizers for lazy iterators."
readme = "README.md"
authors = [{ name="Fulcrum Genomics LLC", email="contact@fulcrumgenomics.com" }]
license = "MIT"
include = ["LICENSE"]
name = "syncup"
version = "0.1.0"
description = "Synchronizers for lazy iterators."
readme = "README.md"
authors = [{ name="Clint Valentine", email="valentine.clint@gmail.com" }]
license = "MIT"
include = ["LICENSE"]
requires-python = ">=3.11"
dependencies = [
"defopt~=6.4"
]
classifiers = [
classifiers = [
"Development Status :: 3 - Alpha",
"Intended Audience :: Developers",
"Intended Audience :: Science/Research",
Expand All @@ -33,9 +30,6 @@ classifiers = [
"repository" = "https://github.com/clintval/syncup"
"Bug Tracker" = "https://github.com/clintval/syncup/issues"

[project.scripts]
syncup = "syncup.main:run"

[dependency-groups]
dev = [
"ruff ==0.12.3",
Expand All @@ -44,30 +38,26 @@ dev = [
"pytest-cov ~=6.2",
"pytest-mock ~=3.14",
"poethepoet ~=0.36",
"hatch ~=1.14",
]
ipython = [
"ipython ~=9.2",
]

[tool.poe.tasks]
fix-format = "ruff format"
fix-lint = "ruff check --fix"
fix-lint = "ruff check --fix"

fix-all.ignore_fail = "return_non_zero"
fix-all.sequence = [
fix-all.sequence = [
"fix-format",
"fix-lint"
]

check-lock = "uv lock --check"
check-lock = "uv lock --check"
check-format = "ruff format --check --diff"
check-lint = "ruff check"
check-tests = "pytest"
check-lint = "ruff check"
check-tests = "pytest"
check-typing = "mypy"

check-all.ignore_fail = "return_non_zero"
check-all.sequence = [
check-all.sequence = [
"check-lock",
"check-format",
"check-lint",
Expand All @@ -76,7 +66,7 @@ check-all.sequence = [
]

fix-and-check-all.ignore_fail = "return_non_zero"
fix-and-check-all.sequence = [
fix-and-check-all.sequence = [
"check-lock",
"fix-format",
"fix-lint",
Expand All @@ -85,51 +75,47 @@ fix-and-check-all.sequence = [
]

[tool.mypy]
files = ["./"]
strict_optional = true
strict_equality = true
check_untyped_defs = true
disallow_incomplete_defs = true
disallow_untyped_calls = true
files = ["./"]
strict_optional = true
strict_equality = true
check_untyped_defs = true
disallow_incomplete_defs = true
disallow_untyped_calls = true
disallow_untyped_decorators = true
disallow_untyped_defs = true
no_implicit_optional = true
warn_no_return = true
warn_redundant_casts = true
warn_return_any = true
warn_unreachable = true
warn_unused_configs = true
warn_unused_ignores = true
enable_error_code = [
disallow_untyped_defs = true
no_implicit_optional = true
warn_no_return = true
warn_redundant_casts = true
warn_return_any = true
warn_unreachable = true
warn_unused_configs = true
warn_unused_ignores = true
enable_error_code = [
"ignore-without-code",
"possibly-undefined",
"exhaustive-match",
]
exclude = [
exclude = [
"docs/",
"site/",
]

[[tool.mypy.overrides]]
module = "defopt"
ignore_missing_imports = true

[tool.pytest.ini_options]
minversion = "7.4"
addopts = [
addopts = [
"--color=yes",
"--import-mode=importlib",
"--cov"
]

[tool.ruff]
line-length = 100
line-length = 100
target-version = "py311"
output-format = "full"
preview = true
output-format = "full"
preview = true

[tool.ruff.lint]
select = [
select = [
"A", # Builtin shadowing
"ARG", # Unused arguments
"C901", # McCabe complexity
Expand All @@ -146,7 +132,7 @@ select = [
"W", # pycodestyle warnings
"Q", # flake8-quotes
]
ignore = [
ignore = [
"E203",
"E701",
"D212", # summary line should be located on the same line as opening quotes
Expand Down
5 changes: 5 additions & 0 deletions syncup/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
from ._syncup import Cmp as Cmp
from ._syncup import Comparable as Comparable
from ._syncup import sync as sync

__all__ = ["sync", "Comparable", "Cmp"]
60 changes: 60 additions & 0 deletions syncup/_syncup.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
from abc import abstractmethod
from typing import Callable
from typing import Iterator
from typing import Protocol
from typing import TypeVar

T = TypeVar("T")
"""A type variable for generic items."""


class Comparable(Protocol):
"""A protocol for comparable objects."""

@abstractmethod
def __lt__(self: T, other: T) -> bool:
"""Less than comparison."""

@abstractmethod
def __gt__(self: T, other: T) -> bool:
"""Greater than comparison."""


Cmp = TypeVar("Cmp", bound=Comparable)
"""A type variable for comparable types."""


def sync(
iter1: Iterator[T],
iter2: Iterator[T],
key1: Callable[[T], Cmp],
key2: Callable[[T], Cmp],
cmp_func: Callable[[Cmp, Cmp], int] = lambda x, y: (x > y) - (x < y),
) -> Iterator[tuple[T, T]]:
"""
Sync two iterators based on a comparison function and key functions.

Args:
iter1: The first iterator.
iter2: The second iterator.
key1: A function to extract the comparison key from items in iter1.
key2: A function to extract the comparison key from items in iter2.
cmp_func: A function to compare two keys, returning negative if the first is less than
the second, zero if they are equal, and positive if the first is greater than
the second. Defaults to the default equality operations.

"""
try:
item1 = next(iter1)
item2 = next(iter2)
while True:
if (result := cmp_func(key1(item1), key2(item2))) == 0:
yield (item1, item2)
item1 = next(iter1)
item2 = next(iter2)
elif result < 0:
item1 = next(iter1)
else:
item2 = next(iter2)
except StopIteration:
return
30 changes: 0 additions & 30 deletions syncup/main.py

This file was deleted.

Empty file removed syncup/tools/__init__.py
Empty file.
11 changes: 0 additions & 11 deletions syncup/tools/hello.py

This file was deleted.

6 changes: 6 additions & 0 deletions tests/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
def test_sync_empty_iterators() -> None:
"""Test sync on empty iterators."""
from syncup import sync

result: list = list(sync(iter([]), iter([]), key1=lambda x: x, key2=lambda x: x))
assert result == []
22 changes: 0 additions & 22 deletions tests/test_main.py

This file was deleted.

Loading