From 2fb90929faa215d20f79359763e935fd74d19b42 Mon Sep 17 00:00:00 2001 From: Jayaram Kancherla Date: Tue, 29 Sep 2026 00:40:25 -0700 Subject: [PATCH 1/3] add dependabot config --- .coveragerc | 4 +- .github/dependabot.yml | 15 ++++ .github/workflows/build-docs.yml | 2 +- .github/workflows/publish-pypi.yml | 112 ++++++++++++++++++----------- .github/workflows/run-tests.yml | 26 +++---- .pre-commit-config.yaml | 15 +++- pyproject.toml | 57 ++++++++++++--- src/iranges/IRanges.py | 8 +++ tox.ini | 76 ++++++-------------- 9 files changed, 186 insertions(+), 129 deletions(-) create mode 100644 .github/dependabot.yml diff --git a/.coveragerc b/.coveragerc index 4102025..7235976 100644 --- a/.coveragerc +++ b/.coveragerc @@ -1,8 +1,8 @@ # .coveragerc to control coverage.py [run] branch = True -source = iranges -# omit = bad_file.py +source = src +omit = tests/* [paths] source = diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..12eea0d --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,15 @@ +version: 2 +updates: + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "weekly" + labels: + - "dependencies" + + - package-ecosystem: "pip" + directory: "/" + schedule: + interval: "weekly" + labels: + - "dependencies" diff --git a/.github/workflows/build-docs.yml b/.github/workflows/build-docs.yml index 0980194..2394edb 100644 --- a/.github/workflows/build-docs.yml +++ b/.github/workflows/build-docs.yml @@ -11,7 +11,7 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 - name: Set up Python 3.12 uses: actions/setup-python@v5 diff --git a/.github/workflows/publish-pypi.yml b/.github/workflows/publish-pypi.yml index 487f7d3..8470178 100644 --- a/.github/workflows/publish-pypi.yml +++ b/.github/workflows/publish-pypi.yml @@ -1,65 +1,91 @@ -name: Publish to PyPI +name: Publish to PyPI and GitHub Pages on: push: - tags: - - "*" + tags: "*" jobs: - build_wheels: - name: Build wheels on ${{ matrix.os }} - runs-on: ${{ matrix.os }} - strategy: - matrix: - # macos-13 is an intel runner, higher macos's are apple silicon - # At some point, maybe get this to work on windows-latest - os: [ubuntu-latest, macos-latest] - + build-and-test: + name: Build and Test + runs-on: ubuntu-latest steps: - - name: Check out repository - uses: actions/checkout@v4 + - uses: actions/checkout@v7 + + - name: Set up Python 3.12 + uses: actions/setup-python@v5 + with: + python-version: 3.12 + + - name: Install tox + run: python -m pip install tox - - name: Build wheels - uses: pypa/cibuildwheel@v4.2.0 - env: - CIBW_ARCHS_LINUX: x86_64 - CIBW_PROJECT_REQUIRES_PYTHON: ">=3.10" - CIBW_SKIP: pp* + - name: Test + run: tox -e default - - uses: actions/upload-artifact@v4 + - name: Build Project + run: tox -e build + + - name: Store the distribution packages + uses: actions/upload-artifact@v4 with: - name: cibw-wheels-${{ matrix.os }}-${{ strategy.job-index }} - path: ./wheelhouse/*.whl + name: python-package-distributions + path: dist/ - build_sdist: - name: Build source distribution + build-docs: + name: Build Documentation runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 + + - name: Set up Python 3.12 + uses: actions/setup-python@v5 with: - submodules: true + python-version: 3.12 + + - name: Install tox + run: python -m pip install tox + + - name: Build docs + run: tox -e docs - - name: Build sdist - run: pipx run build --sdist + - name: Add .nojekyll + run: touch ./docs/_build/html/.nojekyll - - uses: actions/upload-artifact@v4 + - name: Upload Pages artifact + uses: actions/upload-pages-artifact@v5 with: - name: cibw-sdist - path: dist/*.tar.gz + path: ./docs/_build/html - upload_pypi: - needs: [build_wheels, build_sdist] + publish-pypi: + name: Publish to PyPI + needs: build-and-test runs-on: ubuntu-latest + environment: + name: pypi + url: https://pypi.org/p/IRanges permissions: - id-token: write - + id-token: write # IMPORTANT: mandatory for trusted publishing steps: - - uses: actions/download-artifact@v4 + - name: Download all the dists + uses: actions/download-artifact@v8 with: - pattern: cibw-* - path: dist - merge-multiple: true - - # This uses the trusted publisher workflow so no token is required. - - name: Publish to PyPI + name: python-package-distributions + path: dist/ + + - name: Publish package to PyPI uses: pypa/gh-action-pypi-publish@release/v1 + + deploy-pages: + name: Deploy GitHub Pages + needs: build-docs + runs-on: ubuntu-latest + permissions: + pages: write + id-token: write + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + steps: + - name: Deploy to GitHub Pages + id: deployment + uses: actions/deploy-pages@v5 diff --git a/.github/workflows/run-tests.yml b/.github/workflows/run-tests.yml index 79136b3..25083d6 100644 --- a/.github/workflows/run-tests.yml +++ b/.github/workflows/run-tests.yml @@ -31,29 +31,23 @@ jobs: python: ["3.10", "3.11", "3.12", "3.13", "3.14"] platform: - ubuntu-latest - # - macos-latest - # - windows-latest + - macos-latest + - windows-latest runs-on: ${{ matrix.platform }} name: Python ${{ matrix.python }}, ${{ matrix.platform }} steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 - - uses: actions/setup-python@v5 - id: setup-python + - name: Set up Python + uses: actions/setup-python@v5 with: python-version: ${{ matrix.python }} - - name: Install dependencies - run: | - python -m pip install --upgrade pip - pip install tox coverage - - - name: Get latest CMake - uses: lukka/get-cmake@latest + - name: Install tox + run: python -m pip install tox coverage - name: Run tests run: >- - pipx run --python '${{ steps.setup-python.outputs.python-path }}' tox -- -rFEx --durations 10 --color yes --cov --cov-branch --cov-report=xml # pytest args @@ -68,9 +62,9 @@ jobs: fi - name: Upload coverage reports to Codecov with GitHub Action - uses: codecov/codecov-action@v5 + uses: codecov/codecov-action@v7 if: ${{ steps.codecov-check.outputs.codecov == 'true' }} - env: - CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} + with: + token: ${{ secrets.CODECOV_TOKEN }} slug: ${{ github.repository }} flags: ${{ matrix.platform }} - py${{ matrix.python }} diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 3f91485..af3eb03 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -33,10 +33,12 @@ repos: - repo: https://github.com/astral-sh/ruff-pre-commit # Ruff version. - rev: v0.15.6 + rev: v0.16.2 hooks: - - id: ruff - args: [--fix, --exit-non-zero-on-fix] + # Run the linter. + - id: ruff-check + args: [--fix, --exit-zero] + # Run the formatter. - id: ruff-format ## If like to embrace black styles even in the docs: @@ -51,3 +53,10 @@ repos: # rev: v2.2.5 # hooks: # - id: codespell + +- repo: https://github.com/PyCQA/bandit + rev: 1.7.9 + hooks: + - id: bandit + args: ["-c", "pyproject.toml"] + additional_dependencies: ["bandit[toml]"] diff --git a/pyproject.toml b/pyproject.toml index af9afa6..35541aa 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,28 +1,63 @@ [build-system] -# AVOID CHANGING REQUIRES: IT WILL BE UPDATED BY PYSCAFFOLD! -requires = ["setuptools>=46.1.0", "setuptools_scm[toml]>=5", "cmake", "pybind11", "numpy"] +requires = [ + "setuptools>=46.1.0", + "setuptools_scm[toml]>=5", + "cmake", + "pybind11", + "numpy", +] build-backend = "setuptools.build_meta" [tool.setuptools_scm] -# For smarter version schemes and other configuration options, -# check out https://github.com/pypa/setuptools_scm version_scheme = "no-guess-dev" [tool.ruff] line-length = 120 -src = ["src"] -# exclude = ["tests"] -extend-ignore = ["F821"] +src = [ + "src", +] +exclude = [ + "tests", + "docs", +] -[tool.ruff.pydocstyle] +[tool.ruff.lint] +extend-ignore = [ + "F821", +] + +[tool.ruff.lint.pydocstyle] convention = "google" +[tool.ruff.lint.per-file-ignores] +"__init__.py" = [ + "E402", + "F401", +] + [tool.ruff.format] docstring-code-format = true docstring-code-line-length = 20 -[tool.ruff.per-file-ignores] -"__init__.py" = ["E402", "F401"] - [tool.black] force-exclude = "__init__.py" + +[tool.mypy] +strict = true + +[tool.pytest.ini_options] +addopts = "--cov --cov-report term-missing" +testpaths = [ + "tests", +] + +[project.optional-dependencies] +testing = [ + "pytest>=9.1.1", + "pytest-cov>=7.1.0", +] + + +[tool.bandit] +exclude_dirs = ["tests"] +skips = ["B110"] diff --git a/src/iranges/IRanges.py b/src/iranges/IRanges.py index 8fcff1d..2800178 100644 --- a/src/iranges/IRanges.py +++ b/src/iranges/IRanges.py @@ -804,6 +804,14 @@ def reduce( return result + def __xtfrm__(self) -> list: + """Get the transform representation for ordering/sorting. + + Returns: + A list of start-width tuples. + """ + return [(int(s), int(w)) for s, w in zip(self._start, self._width)] + def order(self, decreasing: bool = False) -> np.ndarray: """Get the order of indices for sorting. diff --git a/tox.ini b/tox.ini index 69f8159..ab663e9 100644 --- a/tox.ini +++ b/tox.ini @@ -1,93 +1,63 @@ -# Tox configuration file +# Tox configuration file using uv as the backend runner # Read more under https://tox.wiki/ -# THIS SCRIPT IS SUPPOSED TO BE AN EXAMPLE. MODIFY IT ACCORDING TO YOUR NEEDS! [tox] -minversion = 3.24 +minversion = 4.0 envlist = default -isolated_build = True - [testenv] description = Invoke pytest to run automated tests -setenv = - TOXINIDIR = {toxinidir} -passenv = - HOME - SETUPTOOLS_* -extras = - testing +extras = testing +deps = twine commands = pytest {posargs} +[testenv:typecheck] +deps = mypy +description = Run static type checking with mypy +commands = + mypy src/ -# # To run `tox -e lint` you need to make sure you have a -# # `.pre-commit-config.yaml` file. See https://pre-commit.com -# [testenv:lint] -# description = Perform static analysis and style checks -# skip_install = True -# deps = pre-commit -# passenv = -# HOMEPATH -# PROGRAMDATA -# SETUPTOOLS_* -# commands = -# pre-commit run --all-files {posargs:--show-diff-on-failure} - +[testenv:lint] +description = Perform static analysis and style checks +deps = ruff +skip_install = True +commands = + ruff check {posargs:.} + ruff format --check {posargs:.} [testenv:{build,clean}] description = - build: Build the package in isolation according to PEP517, see https://github.com/pypa/build - clean: Remove old distribution files and temporary build artifacts (./build and ./dist) -# https://setuptools.pypa.io/en/stable/build_meta.html#how-to-use-it + build: Build the package + clean: Remove old distribution files +deps = build skip_install = True -changedir = {toxinidir} -deps = - build: build[virtualenv] -passenv = - SETUPTOOLS_* commands = clean: python -c 'import shutil; [shutil.rmtree(p, True) for p in ("build", "dist", "docs/_build")]' clean: python -c 'import pathlib, shutil; [shutil.rmtree(p, True) for p in pathlib.Path("src").glob("*.egg-info")]' build: python -m build {posargs} -# By default, both `sdist` and `wheel` are built. If your sdist is too big or you don't want -# to make it available, consider running: `tox -e build -- --wheel` - [testenv:{docs,doctests,linkcheck}] description = docs: Invoke sphinx-build to build the docs doctests: Invoke sphinx-build to run doctests linkcheck: Check for broken links in the documentation -passenv = - SETUPTOOLS_* +deps = + -r {toxinidir}/docs/requirements.txt setenv = DOCSDIR = {toxinidir}/docs BUILDDIR = {toxinidir}/docs/_build docs: BUILD = html doctests: BUILD = doctest linkcheck: BUILD = linkcheck -deps = - -r {toxinidir}/docs/requirements.txt - # ^ requirements.txt shared with Read The Docs commands = + sphinx-apidoc -f -o "{env:DOCSDIR}/api" src/ sphinx-build --color -b {env:BUILD} -d "{env:BUILDDIR}/doctrees" "{env:DOCSDIR}" "{env:BUILDDIR}/{env:BUILD}" {posargs} - [testenv:publish] description = Publish the package you have been developing to a package index server. - By default, it uses testpypi. If you really want to publish your package - to be publicly accessible in PyPI, use the `-- --repository pypi` option. skip_install = True -changedir = {toxinidir} -passenv = - # See: https://twine.readthedocs.io/en/latest/ - TWINE_USERNAME - TWINE_PASSWORD - TWINE_REPOSITORY - TWINE_REPOSITORY_URL deps = twine commands = - python -m twine check dist/* - python -m twine upload {posargs:--repository {env:TWINE_REPOSITORY:testpypi}} dist/* + python -m twine upload {posargs:dist/*} From 48193dc6ca65a54fefc0f94900a52e6145831440 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 29 Sep 2026 07:41:37 +0000 Subject: [PATCH 2/3] Update sphinx requirement from >=3.2.1 to >=9.1.0 Updates the requirements on [sphinx](https://github.com/sphinx-doc/sphinx) to permit the latest version. - [Release notes](https://github.com/sphinx-doc/sphinx/releases) - [Changelog](https://github.com/sphinx-doc/sphinx/blob/master/CHANGES.rst) - [Commits](https://github.com/sphinx-doc/sphinx/compare/v3.2.1...v9.1.0) --- updated-dependencies: - dependency-name: sphinx dependency-version: 9.1.0 dependency-type: direct:production ... Signed-off-by: dependabot[bot] --- docs/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/requirements.txt b/docs/requirements.txt index c20cf60..04b5898 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -5,5 +5,5 @@ myst-nb # under `install_requires` in `setup.cfg` is also listed here! # sphinx_rtd_theme myst-parser[linkify] -sphinx>=3.2.1 +sphinx>=9.1.0 sphinx-autodoc-typehints From bc52571fcf2c02178d1e4b89fa52a4f04534b1b4 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 29 Sep 2026 07:41:53 +0000 Subject: [PATCH 3/3] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- .github/workflows/publish-pypi.yml | 2 +- setup.py | 2 +- src/iranges/IRanges.py | 93 +++++++++++++++--------------- src/iranges/__init__.py | 2 +- src/iranges/irangeslist.py | 23 ++++---- src/iranges/sew_handler.py | 10 ++-- src/iranges/utils.py | 14 ++--- 7 files changed, 71 insertions(+), 75 deletions(-) diff --git a/.github/workflows/publish-pypi.yml b/.github/workflows/publish-pypi.yml index 8470178..c6c93f9 100644 --- a/.github/workflows/publish-pypi.yml +++ b/.github/workflows/publish-pypi.yml @@ -71,7 +71,7 @@ jobs: with: name: python-package-distributions path: dist/ - + - name: Publish package to PyPI uses: pypa/gh-action-pypi-publish@release/v1 diff --git a/setup.py b/setup.py index 94d78ce..be7c29b 100644 --- a/setup.py +++ b/setup.py @@ -73,7 +73,7 @@ def build_cmake(self, ext): ext_modules=[CMakeExtension("iranges")], cmdclass={"build_ext": build_ext}, ) - except: # noqa + except: print( "\n\nAn error occurred while building the project, " "please ensure you have the most updated version of setuptools, " diff --git a/src/iranges/IRanges.py b/src/iranges/IRanges.py index 2800178..02ced86 100644 --- a/src/iranges/IRanges.py +++ b/src/iranges/IRanges.py @@ -1,7 +1,8 @@ from __future__ import annotations +from collections.abc import Sequence from copy import deepcopy -from typing import Any, Dict, List, Literal, Optional, Sequence, Tuple, Union +from typing import Any, Literal from warnings import warn import biocutils as ut @@ -63,11 +64,11 @@ class IRanges(ut.BiocObject): def __init__( self, - start: Union[np.ndarray, Sequence[int]] = [], - width: Union[np.ndarray, Sequence[int]] = [], - names: Optional[Union[Sequence[str], ut.Names]] = None, - mcols: Optional[BiocFrame] = None, - metadata: Optional[Union[Dict[str, Any], ut.NamedList]] = None, + start: np.ndarray | Sequence[int] = [], + width: np.ndarray | Sequence[int] = [], + names: Sequence[str] | ut.Names | None = None, + mcols: BiocFrame | None = None, + metadata: dict[str, Any] | ut.NamedList | None = None, _validate: bool = True, ): """ @@ -151,7 +152,7 @@ def _sanitize_names(self, names): def _validate_names(self): if self._names is None: - return None + return if not isinstance(self._names, ut.Names): raise ValueError("'names' should be a list of strings.") @@ -185,7 +186,7 @@ def get_start(self) -> np.ndarray: """ return self._start - def set_start(self, start: Union[np.ndarray, Sequence[int]], in_place: bool = False) -> IRanges: + def set_start(self, start: np.ndarray | Sequence[int], in_place: bool = False) -> IRanges: """Modify start positions (in-place operation). Args: @@ -222,7 +223,7 @@ def start(self) -> np.ndarray: return self.get_start() @start.setter - def start(self, start: Union[np.ndarray, Sequence[int]]): + def start(self, start: np.ndarray | Sequence[int]): """Modify start positions (in-place operation). Args: @@ -245,7 +246,7 @@ def get_width(self) -> np.ndarray: """ return self._width - def set_width(self, width: Union[np.ndarray, Sequence[int]], in_place: bool = False) -> IRanges: + def set_width(self, width: np.ndarray | Sequence[int], in_place: bool = False) -> IRanges: """ Args: width: @@ -282,7 +283,7 @@ def width(self) -> np.ndarray: return self.get_width() @width.setter - def width(self, width: Union[np.ndarray, Sequence[int]]): + def width(self, width: np.ndarray | Sequence[int]): """Set or modify width of each interval (in-place operation). Args: @@ -323,7 +324,7 @@ def end(self) -> np.ndarray: """ return self.get_end() - def get_names(self) -> Optional[ut.Names]: + def get_names(self) -> ut.Names | None: """Get range names. Returns: @@ -332,7 +333,7 @@ def get_names(self) -> Optional[ut.Names]: """ return self._names - def set_names(self, names: Optional[Union[ut.Names, Sequence[str]]], in_place: bool = False) -> IRanges: + def set_names(self, names: ut.Names | Sequence[str] | None, in_place: bool = False) -> IRanges: """ Args: names: @@ -352,7 +353,7 @@ def set_names(self, names: Optional[Union[ut.Names, Sequence[str]]], in_place: b return output @property - def names(self) -> Optional[ut.Names]: + def names(self) -> ut.Names | None: """Get names. Returns: @@ -362,7 +363,7 @@ def names(self) -> Optional[ut.Names]: return self.get_names() @names.setter - def names(self, names: Optional[Sequence[str]]): + def names(self, names: Sequence[str] | None): """Set new names (in-place operation). Args: @@ -388,7 +389,7 @@ def get_mcols(self) -> BiocFrame: """ return self._mcols - def set_mcols(self, mcols: Optional[BiocFrame], in_place: bool = False) -> IRanges: + def set_mcols(self, mcols: BiocFrame | None, in_place: bool = False) -> IRanges: """Set new metadata about ranges. Args: @@ -419,7 +420,7 @@ def mcols(self) -> BiocFrame: return self.get_mcols() @mcols.setter - def mcols(self, mcols: Optional[BiocFrame]): + def mcols(self, mcols: BiocFrame | None): """Set new metadata about ranges (in-place operation). Args: @@ -444,7 +445,7 @@ def __len__(self) -> int: """ return len(self._start) - def __getitem__(self, subset: Union[Sequence, int, str, bool, slice, range]) -> IRanges: + def __getitem__(self, subset: Sequence | int | str | bool | slice | range) -> IRanges: """Subset the IRanges. Args: @@ -465,7 +466,7 @@ def __getitem__(self, subset: Union[Sequence, int, str, bool, slice, range]) -> metadata=self._metadata, ) - def __setitem__(self, args: Union[Sequence, int, str, bool, slice, range], value: IRanges): + def __setitem__(self, args: Sequence | int | str | bool | slice | range, value: IRanges): """Add or update positions (in-place operation). Args: @@ -502,7 +503,7 @@ def __setitem__(self, args: Union[Sequence, int, str, bool, slice, range], value self.delete_nclist_index() - def get_row(self, index_or_name: Union[str, int]) -> IRanges: + def get_row(self, index_or_name: str | int) -> IRanges: """Access a row by index or row name. Args: @@ -677,8 +678,8 @@ def __deepcopy__(self, memo) -> IRanges: ############################# def shift_and_clip_ranges( - self, shift: np.ndarray, width: Union[int, None] = None, circle_length: Union[int, None] = None - ) -> Tuple[np.ndarray, np.ndarray, int, bool]: + self, shift: np.ndarray, width: int | None = None, circle_length: int | None = None + ) -> tuple[np.ndarray, np.ndarray, int, bool]: """Shift and clip interval ranges. Args: @@ -705,10 +706,10 @@ def shift_and_clip_ranges( def coverage( self, - shift: Optional[np.ndarray] = None, - width: Union[int, None] = None, - weight: Optional[np.ndarray] = None, - circle_length: Union[int, None] = None, + shift: np.ndarray | None = None, + width: int | None = None, + weight: np.ndarray | None = None, + circle_length: int | None = None, method: Literal["auto", "sort", "hash", "naive"] = "auto", ) -> np.ndarray: """Compute weighted coverage of ranges. @@ -850,7 +851,7 @@ def sort(self, decreasing: bool = False, in_place: bool = False) -> IRanges: output = self._define_output(in_place) return output[order] - def gaps(self, start: Optional[int] = None, end: Optional[int] = None) -> IRanges: + def gaps(self, start: int | None = None, end: int | None = None) -> IRanges: """Gaps returns an ``IRanges`` object representing the set of intervals that remain after the ranges are removed specified by the start and end arguments. @@ -958,7 +959,7 @@ def disjoint_bins(self) -> np.ndarray: #### intra-range methods #### ############################# - def shift(self, shift: Union[int, List[int], np.ndarray], in_place: bool = False) -> IRanges: + def shift(self, shift: int | list[int] | np.ndarray, in_place: bool = False) -> IRanges: """Shift ranges by specified amount. Args: @@ -990,9 +991,9 @@ def shift(self, shift: Union[int, List[int], np.ndarray], in_place: bool = False def narrow( self, - start: Optional[Union[int, List[int], np.ndarray]] = None, - width: Optional[Union[int, List[int], np.ndarray]] = None, - end: Optional[Union[int, List[int], np.ndarray]] = None, + start: int | list[int] | np.ndarray | None = None, + width: int | list[int] | np.ndarray | None = None, + end: int | list[int] | np.ndarray | None = None, in_place: bool = False, ) -> IRanges: """Narrow ranges. @@ -1035,8 +1036,8 @@ def narrow( def resize( self, - width: Union[int, List[int], np.ndarray], - fix: Union[Literal["start", "end", "center"], List[Literal["start", "end", "center"]]] = "start", + width: int | list[int] | np.ndarray, + fix: Literal["start", "end", "center"] | list[Literal["start", "end", "center"]] = "start", in_place: bool = False, ) -> IRanges: """Resize ranges to the specified ``width`` where either the ``start``, ``end``, or ``center`` is used as an @@ -1301,8 +1302,8 @@ def reflect(self, bounds: IRanges, in_place: bool = False) -> IRanges: def restrict( self, - start: Optional[Union[int, List[int], np.ndarray]] = None, - end: Optional[Union[int, List[int], np.ndarray]] = None, + start: int | list[int] | np.ndarray | None = None, + end: int | list[int] | np.ndarray | None = None, keep_all_ranges: bool = False, ) -> IRanges: """Restrict ranges to a given start and end positions. @@ -1378,10 +1379,10 @@ def restrict( def threebands( self, - start: Optional[Union[int, np.ndarray]] = None, - end: Optional[Union[int, np.ndarray]] = None, - width: Optional[Union[int, np.ndarray]] = None, - ) -> Dict[str, IRanges]: + start: int | np.ndarray | None = None, + end: int | np.ndarray | None = None, + width: int | np.ndarray | None = None, + ) -> dict[str, IRanges]: """Split ranges into three parts: left, middle, and right. Args: @@ -1425,7 +1426,7 @@ def threebands( "right": IRanges(right_starts, right_widths), } - def overlap_indices(self, start: Optional[int] = None, end: Optional[int] = None) -> np.ndarray: + def overlap_indices(self, start: int | None = None, end: int | None = None) -> np.ndarray: """Find overlaps with the start and end positions. Args: @@ -1876,7 +1877,7 @@ def precede( select: Literal["all", "first"] = "first", delete_index: bool = True, num_threads: int = 1, - ) -> Union[np.ndarray, BiocFrame]: + ) -> np.ndarray | BiocFrame: """Find nearest positions that are upstream/precede each query range. Args: @@ -1938,7 +1939,7 @@ def follow( select: Literal["all", "last"] = "last", delete_index: bool = True, num_threads: int = 1, - ) -> Union[np.ndarray, BiocFrame]: + ) -> np.ndarray | BiocFrame: """Find nearest positions that are downstream/follow each query range. Args: @@ -2021,7 +2022,7 @@ def nearest( adjacent_equals_overlap: bool = True, delete_index: bool = True, num_threads: int = 1, - ) -> Union[np.ndarray, BiocFrame]: + ) -> np.ndarray | BiocFrame: """Find nearest ranges in both directions. Args: @@ -2249,9 +2250,7 @@ def combine(self, *other: IRanges) -> IRanges: ######>> window methods <<###### ################################ - def tile( - self, n: Optional[Union[int, np.ndarray]] = None, width: Optional[Union[int, np.ndarray]] = None - ) -> List[IRanges]: + def tile(self, n: int | np.ndarray | None = None, width: int | np.ndarray | None = None) -> list[IRanges]: """Split ranges into either n equal parts or parts of fixed width. Args: @@ -2305,7 +2304,7 @@ def tile( return result - def sliding_windows(self, width: int, step: int = 1) -> List[IRanges]: + def sliding_windows(self, width: int, step: int = 1) -> list[IRanges]: """Create sliding windows of fixed width and step size. Args: diff --git a/src/iranges/__init__.py b/src/iranges/__init__.py index e5b3558..8a59905 100644 --- a/src/iranges/__init__.py +++ b/src/iranges/__init__.py @@ -16,5 +16,5 @@ del version, PackageNotFoundError from .IRanges import IRanges -from .utils import normalize_array from .irangeslist import CompressedIRangesList +from .utils import normalize_array diff --git a/src/iranges/irangeslist.py b/src/iranges/irangeslist.py index d314fd1..9329eb6 100644 --- a/src/iranges/irangeslist.py +++ b/src/iranges/irangeslist.py @@ -1,6 +1,7 @@ from __future__ import annotations -from typing import Any, Dict, List, Optional, Sequence, Union +from collections.abc import Sequence +from typing import Any import biocutils as ut from compressed_lists import CompressedList, Partitioning @@ -20,8 +21,8 @@ def __init__( self, unlist_data: IRanges, partitioning: Partitioning, - element_metadata: Optional[dict] = None, - metadata: Optional[Union[Dict[str, Any], ut.NamedList]] = None, + element_metadata: dict | None = None, + metadata: dict[str, Any] | ut.NamedList | None = None, **kwargs, ): """Initialize a CompressedIRangesList. @@ -52,9 +53,9 @@ def __init__( @classmethod def from_list( cls, - lst: List[IRanges], - names: Optional[Union[ut.Names, Sequence[str]]] = None, - metadata: Optional[Union[Dict[str, Any], ut.NamedList]] = None, + lst: list[IRanges], + names: ut.Names | Sequence[str] | None = None, + metadata: dict[str, Any] | ut.NamedList | None = None, ) -> CompressedIRangesList: """Create a `CompressedIRangesList` from a regular list. @@ -141,8 +142,8 @@ def __str__(self) -> str: output += f"partitioning: {ut.print_truncated_list(self._partitioning)}\n" - output += f"element_metadata({str(len(self._element_metadata))} rows): {ut.print_truncated_list(list(self._element_metadata.get_column_names()), sep=' ', include_brackets=False, transform=lambda y: y)}\n" - output += f"metadata({str(len(self._metadata))}): {ut.print_truncated_list(list(self._metadata.keys()), sep=' ', include_brackets=False, transform=lambda y: y)}\n" + output += f"element_metadata({len(self._element_metadata)!s} rows): {ut.print_truncated_list(list(self._element_metadata.get_column_names()), sep=' ', include_brackets=False, transform=lambda y: y)}\n" + output += f"metadata({len(self._metadata)!s}): {ut.print_truncated_list(list(self._metadata.keys()), sep=' ', include_brackets=False, transform=lambda y: y)}\n" return output @@ -150,9 +151,9 @@ def __str__(self) -> str: @splitAsCompressedList.register def _( data: IRanges, - groups_or_partitions: Union[list, Partitioning], - names: Optional[Union[ut.Names, Sequence[str]]] = None, - metadata: Optional[dict] = None, + groups_or_partitions: list | Partitioning, + names: ut.Names | Sequence[str] | None = None, + metadata: dict | None = None, ) -> CompressedIRangesList: """Handle lists of IRanges objects.""" diff --git a/src/iranges/sew_handler.py b/src/iranges/sew_handler.py index 9a9fa20..5b4e70b 100644 --- a/src/iranges/sew_handler.py +++ b/src/iranges/sew_handler.py @@ -1,5 +1,3 @@ -from typing import Optional, Tuple, Union - import numpy as np from .utils import handle_negative_coords, normalize_array @@ -17,9 +15,9 @@ class SEWWrangler: def __init__( self, ref_widths: np.ndarray, - start: Optional[Union[int, np.ndarray]] = None, - end: Optional[Union[int, np.ndarray]] = None, - width: Optional[Union[int, np.ndarray]] = None, + start: int | np.ndarray | None = None, + end: int | np.ndarray | None = None, + width: int | np.ndarray | None = None, translate_negative: bool = True, allow_nonnarrowing: bool = False, ): @@ -79,7 +77,7 @@ def _validate_narrowing(self, starts: np.ndarray, widths: np.ndarray) -> None: f"the solved end ({int(ends[idx])}) is > refwidth" ) - def solve(self) -> Tuple[np.ndarray, np.ndarray]: + def solve(self) -> tuple[np.ndarray, np.ndarray]: """Resolve Start/End/Width parameters to concrete ranges. Returns: diff --git a/src/iranges/utils.py b/src/iranges/utils.py index b51afff..910766a 100644 --- a/src/iranges/utils.py +++ b/src/iranges/utils.py @@ -1,5 +1,3 @@ -from typing import Optional, Tuple, Union - import numpy as np __author__ = "Jayaram Kancherla" @@ -8,7 +6,7 @@ def normalize_array( - x: Optional[Union[int, float, np.number, np.ndarray]], length: int, dtype: np.dtype = np.int32 + x: float | np.number | np.ndarray | None, length: int, dtype: np.dtype = np.int32 ) -> np.ma.MaskedArray: """Normalize input to masked array with proper length and type. @@ -66,8 +64,8 @@ def handle_negative_coords(coords: np.ma.MaskedArray, ref_len: np.ndarray) -> np def clip_ranges( - starts: np.ndarray, widths: np.ndarray, min_val: Optional[int] = None, max_val: Optional[int] = None -) -> Tuple[np.ndarray, np.ndarray]: + starts: np.ndarray, widths: np.ndarray, min_val: int | None = None, max_val: int | None = None +) -> tuple[np.ndarray, np.ndarray]: """Clip ranges to specified bounds. Args: @@ -102,10 +100,10 @@ def clip_ranges( def compute_up_down( starts: np.ndarray, widths: np.ndarray, - upstream: Union[int, np.ndarray], - downstream: Union[int, np.ndarray], + upstream: int | np.ndarray, + downstream: int | np.ndarray, site: str, -) -> Tuple[np.ndarray, np.ndarray]: +) -> tuple[np.ndarray, np.ndarray]: """Helper for promoters/terminators.""" length = len(starts)