From c03bb35e5418c3e757c8d6ba67189c2b289600b0 Mon Sep 17 00:00:00 2001 From: Jayaram Kancherla Date: Fri, 20 Dec 2024 11:20:32 -0800 Subject: [PATCH] chore: remove Python 3.8 support (#47) --- .github/workflows/pypi-publish.yml | 8 +- .github/workflows/pypi-test.yml | 6 +- .pre-commit-config.yaml | 13 +-- CHANGELOG.md | 5 ++ docs/conf.py | 1 + pyproject.toml | 4 + setup.cfg | 2 +- src/iranges/IRanges.py | 140 +++++++---------------------- src/iranges/interval.py | 9 +- 9 files changed, 61 insertions(+), 127 deletions(-) diff --git a/.github/workflows/pypi-publish.yml b/.github/workflows/pypi-publish.yml index 7b591a2..030cd10 100644 --- a/.github/workflows/pypi-publish.yml +++ b/.github/workflows/pypi-publish.yml @@ -13,11 +13,11 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 - - name: Set up Python 3.9 - uses: actions/setup-python@v2 + - uses: actions/checkout@v4 + - name: Set up Python 3.11 + uses: actions/setup-python@v5 with: - python-version: 3.9 + python-version: 3.11 - name: Install dependencies run: | python -m pip install --upgrade pip diff --git a/.github/workflows/pypi-test.yml b/.github/workflows/pypi-test.yml index 9dc019a..22f6c4a 100644 --- a/.github/workflows/pypi-test.yml +++ b/.github/workflows/pypi-test.yml @@ -15,13 +15,13 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python-version: [ '3.8', '3.9', '3.10', '3.11', '3.12' ] + python-version: [ '3.9', '3.10', '3.11', '3.12', '3.13' ] name: Python ${{ matrix.python-version }} steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 - name: Setup Python - uses: actions/setup-python@v2 + uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} cache: 'pip' diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index eed031a..e60a5f4 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -18,18 +18,18 @@ repos: args: ['--fix=auto'] # replace 'auto' with 'lf' to enforce Linux/Mac line endings or 'crlf' for Windows # - repo: https://github.com/PyCQA/docformatter -# rev: v1.7.5 +# rev: master # hooks: # - id: docformatter # additional_dependencies: [tomli] # args: [--in-place, --wrap-descriptions=120, --wrap-summaries=120] # # --config, ./pyproject.toml -- repo: https://github.com/psf/black - rev: 24.8.0 - hooks: - - id: black - language_version: python3 +# - repo: https://github.com/psf/black +# rev: 24.8.0 +# hooks: +# - id: black +# language_version: python3 - repo: https://github.com/astral-sh/ruff-pre-commit # Ruff version. @@ -37,6 +37,7 @@ repos: hooks: - id: ruff args: [--fix, --exit-non-zero-on-fix] + - id: ruff-format ## If like to embrace black styles even in the docs: # - repo: https://github.com/asottile/blacken-docs diff --git a/CHANGELOG.md b/CHANGELOG.md index 24482ac..981f5f6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,10 @@ # Changelog +## Version 0.3.0 + +- chore: Remove Python 3.8 (EOL) +- precommit: Replace docformatter with ruff's formatter + ## Version 0.2.10 - 0.2.12 - Added a numpy vectorized version of finding gaps (tldr: not fast compared to the traditional version). May be needs a better implementation diff --git a/docs/conf.py b/docs/conf.py index 131ad0f..a86155f 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -72,6 +72,7 @@ "sphinx.ext.ifconfig", "sphinx.ext.mathjax", "sphinx.ext.napoleon", + "sphinx_autodoc_typehints", ] # Add any paths that contain templates here, relative to this directory. diff --git a/pyproject.toml b/pyproject.toml index 0514df9..45716dd 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -17,6 +17,10 @@ extend-ignore = ["F821"] [tool.ruff.pydocstyle] convention = "google" +[tool.ruff.format] +docstring-code-format = true +docstring-code-line-length = 20 + [tool.ruff.per-file-ignores] "__init__.py" = ["E402", "F401"] diff --git a/setup.cfg b/setup.cfg index dea8aa4..65ad5a9 100644 --- a/setup.cfg +++ b/setup.cfg @@ -41,7 +41,7 @@ package_dir = =src # Require a min/specific Python version (comma-separated conditions) -python_requires = >=3.8 +python_requires = >=3.9 # Add here dependencies of your project (line-separated), e.g. requests>=2.2,<3.0. # Version specifiers like >=2.2,<3.0 avoid problems due to API changes in diff --git a/src/iranges/IRanges.py b/src/iranges/IRanges.py index ec84414..feee725 100644 --- a/src/iranges/IRanges.py +++ b/src/iranges/IRanges.py @@ -40,11 +40,7 @@ def __iter__(self): def __next__(self): if self._current_index < len(self._iranges): - iter_row_index = ( - self._iranges.names[self._current_index] - if self._iranges.names is not None - else None - ) + iter_row_index = self._iranges.names[self._current_index] if self._iranges.names is not None else None iter_slice = self._iranges.get_row(self._current_index) self._current_index += 1 @@ -159,9 +155,7 @@ def _validate_mcols(self): raise TypeError("'mcols' should be a BiocFrame") if self._mcols.shape[0] != len(self._start): - raise ValueError( - "Number of rows in 'mcols' should be equal to length of 'start'" - ) + raise ValueError("Number of rows in 'mcols' should be equal to length of 'start'") def _sanitize_metadata(self, metadata): if metadata is None: @@ -315,9 +309,7 @@ def get_names(self) -> Optional[Names]: """ return self._names - def set_names( - self, names: Optional[Sequence[str]], in_place: bool = False - ) -> "IRanges": + def set_names(self, names: Optional[Sequence[str]], in_place: bool = False) -> "IRanges": """ Args: names: @@ -373,9 +365,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: Optional[BiocFrame], in_place: bool = False) -> "IRanges": """Set new metadata about ranges. Args: @@ -428,9 +418,7 @@ def get_metadata(self) -> dict: """ return self._metadata - def set_metadata( - self, metadata: Optional[dict], in_place: bool = False - ) -> "IRanges": + def set_metadata(self, metadata: Optional[dict], in_place: bool = False) -> "IRanges": """Set or replace metadata. Args: @@ -490,9 +478,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: Union[Sequence, int, str, bool, slice, range]) -> "IRanges": """Subset the IRanges. Args: @@ -513,9 +499,7 @@ def __getitem__( metadata=self._metadata, ) - def __setitem__( - self, args: Union[Sequence, int, str, bool, slice, range], value: "IRanges" - ): + def __setitem__(self, args: Union[Sequence, int, str, bool, slice, range], value: "IRanges"): """Add or update positions (in-place operation). Args: @@ -619,9 +603,7 @@ def __str__(self) -> str: data = self._mcols.column(col) showed = show_as_cell(data, indices) header = [col, "<" + ut.print_type(data) + ">"] - showed = ut.truncate_strings( - showed, width=max(40, len(header[0]), len(header[1])) - ) + showed = ut.truncate_strings(showed, width=max(40, len(header[0]), len(header[1]))) if insert_ellipsis: showed = showed[:3] + ["..."] + showed[3:] columns.append(header + showed) @@ -781,9 +763,7 @@ def clip_intervals( _width = val.width[0] _pshift = shift if isinstance(shift, int) else _ashift[counter] - _pwidth = ( - width if width is None or isinstance(width, int) else _awidth[counter] - ) + _pwidth = width if width is None or isinstance(width, int) else _awidth[counter] if _pshift > 0: _start += _pshift @@ -953,9 +933,7 @@ def order(self, decreasing: bool = False) -> np.ndarray: Returns: NumPy vector containing index positions in the sorted order. """ - order_buf = sorted( - range(len(self)), key=lambda i: (self._start[i], self._width[i]) - ) + order_buf = sorted(range(len(self)), key=lambda i: (self._start[i], self._width[i])) if decreasing: return np.asarray(order_buf[::-1]) @@ -1038,9 +1016,7 @@ def gaps(self, start: Optional[int] = None, end: Optional[int] = None) -> "IRang return IRanges(_gapstarts, _gapends) - def gaps_numpy( - self, start: Optional[int] = None, end: Optional[int] = None - ) -> "IRanges": + def gaps_numpy(self, start: Optional[int] = None, end: Optional[int] = None) -> "IRanges": """Gaps returns an ``IRanges`` object representing the set of integers that remain after the intervals are removed specified by the start and end arguments. @@ -1163,9 +1139,7 @@ def disjoin(self, with_reverse_map: bool = False) -> "IRanges": #### intra range methods #### ############################# - def shift( - self, shift: Union[int, List[int], np.ndarray], in_place: bool = False - ) -> "IRanges": + def shift(self, shift: Union[int, List[int], np.ndarray], in_place: bool = False) -> "IRanges": """Shifts all the intervals by the amount specified by the ``shift`` argument. Args: @@ -1225,23 +1199,15 @@ def narrow( end = self._sanitize_vec_argument(end, allow_none=True) width = self._sanitize_vec_argument(width, allow_none=True) - if (all(x is not None for x in (start, end, width))) or ( - all(x is None for x in (start, end, width)) - ): - raise ValueError( - "Two out of three ('start', 'end' or 'width') arguments must be provided." - ) + if (all(x is not None for x in (start, end, width))) or (all(x is None for x in (start, end, width))): + raise ValueError("Two out of three ('start', 'end' or 'width') arguments must be provided.") if width is not None: - if (isinstance(width, int) and width < 0) or ( - isinstance(width, np.ndarray) and any(x < 0 for x in width) - ): + if (isinstance(width, int) and width < 0) or (isinstance(width, np.ndarray) and any(x < 0 for x in width)): raise ValueError("'width' cannot be negative.") if start is None and end is None: - raise ValueError( - "If 'width' is provided, either 'start' or 'end' must be provided." - ) + raise ValueError("If 'width' is provided, either 'start' or 'end' must be provided.") output = self._define_output(in_place) @@ -1253,18 +1219,12 @@ def narrow( _width = value.width[0] _oend = value.end[0] - _pstart = ( - start if start is None or isinstance(start, int) else start[counter] - ) - _pwidth = ( - width if width is None or isinstance(width, int) else width[counter] - ) + _pstart = start if start is None or isinstance(start, int) else start[counter] + _pwidth = width if width is None or isinstance(width, int) else width[counter] _pend = end if end is None or isinstance(end, int) else end[counter] if _pend is not None and _pend > 0 and _pend > _width: - raise ValueError( - f"Provided 'end' is greater than width of the interval for: {counter}" - ) + raise ValueError(f"Provided 'end' is greater than width of the interval for: {counter}") if _pstart is not None: if _pstart > 0: @@ -1294,9 +1254,7 @@ def narrow( _width = _width + _pend + 1 if _width < 0: - raise ValueError( - f"Provided 'start' or 'end' arguments lead to negative width for interval: {counter}." - ) + raise ValueError(f"Provided 'start' or 'end' arguments lead to negative width for interval: {counter}.") new_starts.append(_start) new_widths.append(_width) @@ -1310,9 +1268,7 @@ def narrow( def resize( self, width: Union[int, List[int], np.ndarray], - fix: Union[ - Literal["start", "end", "center"], List[Literal["start", "end", "center"]] - ] = "start", + fix: Union[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 @@ -1380,14 +1336,10 @@ def resize( output = self._define_output(in_place) output._start = np.asarray(new_starts) - output._width = ( - np.repeat(_awidth, len(self)) if isinstance(_awidth, int) else _awidth - ) + output._width = np.repeat(_awidth, len(self)) if isinstance(_awidth, int) else _awidth return output - def flank( - self, width: int, start: bool = True, both: bool = False, in_place: bool = False - ) -> "IRanges": + def flank(self, width: int, start: bool = True, both: bool = False, in_place: bool = False) -> "IRanges": """Compute flanking ranges for each range. The logic is from the `IRanges` package. If ``start`` is ``True`` for a given range, the flanking occurs at the `start`, @@ -1460,9 +1412,7 @@ def flank( return output - def promoters( - self, upstream: int = 2000, downstream: int = 200, in_place: bool = False - ) -> "IRanges": + def promoters(self, upstream: int = 2000, downstream: int = 200, in_place: bool = False) -> "IRanges": """Extend intervals to promoter regions. Generates promoter ranges relative to the transcription start site (TSS), @@ -1585,9 +1535,7 @@ def restrict( return IRanges(new_starts, new_widths, validate=False) - def overlap_indices( - self, start: Optional[int] = None, end: Optional[int] = None - ) -> np.ndarray: + def overlap_indices(self, start: Optional[int] = None, end: Optional[int] = None) -> np.ndarray: """Find overlaps with the start and end positions. Args: @@ -1724,9 +1672,7 @@ def intersect_ncls(self, other: "IRanges", delete_index: bool = True) -> "IRange other._build_ncls_index() - self_indexes, other_indexes = other._ncls.all_overlaps_both( - self.start, self.end, np.arange(len(self)) - ) + self_indexes, other_indexes = other._ncls.all_overlaps_both(self.start, self.end, np.arange(len(self))) if delete_index: other._delete_ncls_index() @@ -1734,16 +1680,12 @@ def intersect_ncls(self, other: "IRanges", delete_index: bool = True) -> "IRange self_new_starts = self.start[self_indexes] other_new_starts = other.start[other_indexes] - new_starts = np.where( - self_new_starts > other_new_starts, self_new_starts, other_new_starts - ) + new_starts = np.where(self_new_starts > other_new_starts, self_new_starts, other_new_starts) self_new_ends = self.end[self_indexes] other_new_ends = other.end[other_indexes] - new_ends = np.where( - self_new_ends < other_new_ends, self_new_ends, other_new_ends - ) + new_ends = np.where(self_new_ends < other_new_ends, self_new_ends, other_new_ends) return IRanges(new_starts, new_ends - new_starts).reduce() @@ -1865,14 +1807,10 @@ def find_overlaps( raise TypeError("'query' is not a `IRanges` object.") if query_type not in ["any", "start", "end", "within"]: - raise ValueError( - f"'query_type' must be one of {', '.join(['any', 'start', 'end', 'within'])}." - ) + raise ValueError(f"'query_type' must be one of {', '.join(['any', 'start', 'end', 'within'])}.") if select not in ["all", "first", "last", "arbitrary"]: - raise ValueError( - f"'select' must be one of {', '.join(['all', 'first', 'last', 'arbitrary'])}." - ) + raise ValueError(f"'select' must be one of {', '.join(['all', 'first', 'last', 'arbitrary'])}.") _tgap = 0 if max_gap == -1 else max_gap @@ -2032,14 +1970,8 @@ def _generic_search( val.end[0] + (counter * step_end) + 1 > max_end + 1 and val.start[0] - (counter * step_start) - 1 < min_start - 1 ) - or ( - step_end == 0 - and val.start[0] - (counter * step_start) - 1 < min_start - 1 - ) - or ( - step_start == 0 - and val.end[0] + (counter * step_end) + 1 > max_end + 1 - ) + or (step_end == 0 and val.start[0] - (counter * step_start) - 1 < min_start - 1) + or (step_start == 0 and val.end[0] + (counter * step_end) + 1 > max_end + 1) ): _iterate = False _hits = [] @@ -2082,9 +2014,7 @@ def nearest( raise TypeError("`query` is not a `IRanges` object.") if select not in ["all", "arbitrary"]: - raise ValueError( - f"'select' must be one of {', '.join(['all', 'arbitrary'])}." - ) + raise ValueError(f"'select' must be one of {', '.join(['all', 'arbitrary'])}.") hits = self._generic_search(query, 1, 1, 10000000, 1, select, delete_index) self._delete_ncls_index() @@ -2189,9 +2119,7 @@ def distance(self, query: "IRanges") -> np.ndarray: for i in range(len(self)): i_self = self[i] i_query = query[i] - _gap, _overlap = calc_gap_and_overlap( - (i_self.start[0], i_self.end[0]), (i_query.start[0], i_query.end[0]) - ) + _gap, _overlap = calc_gap_and_overlap((i_self.start[0], i_self.end[0]), (i_query.start[0], i_query.end[0])) distance = _gap if _gap is None: diff --git a/src/iranges/interval.py b/src/iranges/interval.py index b7294ee..37aca73 100644 --- a/src/iranges/interval.py +++ b/src/iranges/interval.py @@ -66,18 +66,13 @@ def create_np_interval_vector( cov[_start:_end] += value if with_reverse_map: - _ = [ - revmap[x].append(name if name is not None else counter + 1) - for x in range(_start, _end) - ] + _ = [revmap[x].append(name if name is not None else counter + 1) for x in range(_start, _end)] counter += 1 return cov[1:], revmap -def calc_gap_and_overlap( - first: Tuple[int, int], second: Tuple[int, int] -) -> Tuple[Optional[int], Optional[int]]: +def calc_gap_and_overlap(first: Tuple[int, int], second: Tuple[int, int]) -> Tuple[Optional[int], Optional[int]]: """Calculate gap and/or overlap between two intervals. Args: