From aaca1795c913e5d03e721ee17a2faa6b45e14552 Mon Sep 17 00:00:00 2001 From: Jayaram Kancherla Date: Fri, 20 Dec 2024 11:54:47 -0800 Subject: [PATCH] chore: remove Python 3.8 support (#50) --- .github/workflows/pypi-publish.yml | 75 +++++------ .github/workflows/pypi-test.yml | 47 +++---- .pre-commit-config.yaml | 25 ++-- CHANGELOG.md | 5 + pyproject.toml | 4 + setup.cfg | 2 +- .../SingleCellExperiment.py | 116 +++++------------- src/singlecellexperiment/io/tenx.py | 8 +- 8 files changed, 116 insertions(+), 166 deletions(-) diff --git a/.github/workflows/pypi-publish.yml b/.github/workflows/pypi-publish.yml index 7b591a2..29657bb 100644 --- a/.github/workflows/pypi-publish.yml +++ b/.github/workflows/pypi-publish.yml @@ -9,43 +9,44 @@ on: jobs: build: - runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 - - name: Set up Python 3.9 - uses: actions/setup-python@v2 - with: - python-version: 3.9 - - name: Install dependencies - run: | - python -m pip install --upgrade pip - pip install flake8 pytest tox - # - name: Lint with flake8 - # run: | - # # stop the build if there are Python syntax errors or undefined names - # flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics - # # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide - # # flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics - - name: Test with tox - run: | - tox - - name: Build docs - run: | - tox -e docs - - run: touch ./docs/_build/html/.nojekyll - - name: GH Pages Deployment - uses: JamesIves/github-pages-deploy-action@4.1.3 - with: - branch: gh-pages # The branch the action should deploy to. - folder: ./docs/_build/html - clean: true # Automatically remove deleted files from the deploy branch - - name: Build Project and Publish - run: | - python -m tox -e clean,build - - name: Publish package - uses: pypa/gh-action-pypi-publish@27b31702a0e7fc50959f5ad993c78deac1bdfc29 - with: - user: __token__ - password: ${{ secrets.PYPI_PASSWORD }} + - uses: actions/checkout@v4 + + - name: Set up Python 3.11 + uses: actions/setup-python@v5 + with: + python-version: 3.11 + + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install tox + + - name: Test with tox + run: | + tox + + - name: Build docs + run: | + tox -e docs + + - run: touch ./docs/_build/html/.nojekyll + + - name: GH Pages Deployment + uses: JamesIves/github-pages-deploy-action@v4 + with: + branch: gh-pages # The branch the action should deploy to. + folder: ./docs/_build/html + clean: true # Automatically remove deleted files from the deploy branch + + - name: Build Project and Publish + run: | + python -m tox -e clean,build + + - name: Publish package + uses: pypa/gh-action-pypi-publish@v1.12.2 + with: + user: __token__ + password: ${{ secrets.PYPI_PASSWORD }} diff --git a/.github/workflows/pypi-test.yml b/.github/workflows/pypi-test.yml index 9dc019a..90aa16a 100644 --- a/.github/workflows/pypi-test.yml +++ b/.github/workflows/pypi-test.yml @@ -1,40 +1,33 @@ -# This workflow will install Python dependencies, run tests and lint with a single version of Python -# For more information see: https://help.github.com/actions/language-and-framework-guides/using-python-with-github-actions - -name: Test the library +name: Run tests on: push: - branches: [ master ] + branches: [master] pull_request: - branches: [ master ] + branches: [master] jobs: build: - 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 - - name: Setup Python - uses: actions/setup-python@v2 - with: - python-version: ${{ matrix.python-version }} - cache: 'pip' - - name: Install dependencies - run: | - python -m pip install --upgrade pip - pip install flake8 pytest tox - # - name: Lint with flake8 - # run: | - # # stop the build if there are Python syntax errors or undefined names - # flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics - # # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide - # # flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics - - name: Test with tox - run: | - tox + - uses: actions/checkout@v4 + + - name: Setup Python + uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + cache: "pip" + + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install tox + + - name: Test with tox + run: | + tox diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 3c9601c..e60a5f4 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -17,19 +17,19 @@ repos: - id: mixed-line-ending 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 - hooks: - - id: docformatter - additional_dependencies: [tomli] - args: [--in-place, --wrap-descriptions=120, --wrap-summaries=120] - # --config, ./pyproject.toml +# - repo: https://github.com/PyCQA/docformatter +# 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 5524118..04e1ab8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,10 @@ # Changelog +## Version 0.5.0 + +- chore: Remove Python 3.8 (EOL) +- precommit: Replace docformatter with ruff's formatter + ## Version 0.4.7 - Fix package version issues to support Python<=3.9. Mostly related to how anndata dependencies are versioned in the MuData package discussed [here](https://github.com/scverse/mudata/issues/82). diff --git a/pyproject.toml b/pyproject.toml index a7cea75..00aa968 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -16,6 +16,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 8b9ca8b..1747cdb 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/singlecellexperiment/SingleCellExperiment.py b/src/singlecellexperiment/SingleCellExperiment.py index 47df2ee..3d9434a 100644 --- a/src/singlecellexperiment/SingleCellExperiment.py +++ b/src/singlecellexperiment/SingleCellExperiment.py @@ -30,9 +30,7 @@ def _validate_reduced_dims(reduced_dims, shape): if reduced_dims is None: - raise ValueError( - "'reduced_dims' cannot be `None`, must be assigned to an empty dictionary." - ) + raise ValueError("'reduced_dims' cannot be `None`, must be assigned to an empty dictionary.") if not isinstance(reduced_dims, dict): raise TypeError("'reduced_dims' is not a dictionary.") @@ -40,21 +38,16 @@ def _validate_reduced_dims(reduced_dims, shape): for rdname, mat in reduced_dims.items(): if not hasattr(mat, "shape"): raise TypeError( - f"Reduced dimension: '{rdname}' must be a matrix-like object." - "Does not contain a `shape` property." + f"Reduced dimension: '{rdname}' must be a matrix-like object." "Does not contain a `shape` property." ) if shape[1] != mat.shape[0]: - raise ValueError( - f"Reduced dimension: '{rdname}' does not contain embeddings for all cells." - ) + raise ValueError(f"Reduced dimension: '{rdname}' does not contain embeddings for all cells.") def _validate_alternative_experiments(alternative_experiments, shape): if alternative_experiments is None: - raise ValueError( - "'alternative_experiments' cannot be `None`, must be assigned to an empty dictionary." - ) + raise ValueError("'alternative_experiments' cannot be `None`, must be assigned to an empty dictionary.") if not isinstance(alternative_experiments, dict): raise TypeError("'alternative_experiments' is not a dictionary.") @@ -67,10 +60,7 @@ def _validate_alternative_experiments(alternative_experiments, shape): ) if shape[1] != alternative_experiment.shape[1]: - raise ValueError( - f"Alternative experiment: '{alt_name}' does not contain same number of" - " cells." - ) + raise ValueError(f"Alternative experiment: '{alt_name}' does not contain same number of" " cells.") def _validate_pairs(pairs): @@ -204,18 +194,14 @@ def __init__( self._reduced_dims = reduced_dims if reduced_dims is not None else {} - self._alternative_experiments = ( - alternative_experiments if alternative_experiments is not None else {} - ) + self._alternative_experiments = alternative_experiments if alternative_experiments is not None else {} self._row_pairs = row_pairs if row_pairs is not None else {} self._column_pairs = column_pairs if column_pairs is not None else {} if validate: _validate_reduced_dims(self._reduced_dims, self._shape) - _validate_alternative_experiments( - self._alternative_experiments, self._shape - ) + _validate_alternative_experiments(self._alternative_experiments, self._shape) _validate_pairs(self._row_pairs) _validate_pairs(self._column_pairs) @@ -309,14 +295,10 @@ def __repr__(self) -> str: output += ", row_ranges=" + self._row_ranges.__repr__() if self._alternative_experiments is not None: - output += ", alternative_experiments=" + ut.print_truncated_list( - self.alternative_experiment_names - ) + output += ", alternative_experiments=" + ut.print_truncated_list(self.alternative_experiment_names) if self._reduced_dims is not None: - output += ", reduced_dims=" + ut.print_truncated_list( - self.reduced_dim_names - ) + output += ", reduced_dims=" + ut.print_truncated_list(self.reduced_dim_names) if self._main_experiment_name is not None: output += ", main_experiment_name=" + self._main_experiment_name @@ -344,10 +326,14 @@ def __str__(self) -> str: output += f"assays({len(self.assay_names)}): {ut.print_truncated_list(self.assay_names)}\n" - output += f"row_data columns({len(self._rows.column_names)}): {ut.print_truncated_list(self._rows.column_names)}\n" + output += ( + f"row_data columns({len(self._rows.column_names)}): {ut.print_truncated_list(self._rows.column_names)}\n" + ) output += f"row_names({0 if self._row_names is None else len(self._row_names)}): {' ' if self._row_names is None else ut.print_truncated_list(self._row_names)}\n" - output += f"column_data columns({len(self._cols.column_names)}): {ut.print_truncated_list(self._cols.column_names)}\n" + output += ( + f"column_data columns({len(self._cols.column_names)}): {ut.print_truncated_list(self._cols.column_names)}\n" + ) output += f"column_names({0 if self._column_names is None else len(self._column_names)}): {' ' if self._column_names is None else ut.print_truncated_list(self._column_names)}\n" output += f"main_experiment_name: {' ' if self._main_experiment_name is None else self._main_experiment_name}\n" @@ -373,9 +359,7 @@ def get_reduced_dims(self) -> Dict[str, Any]: """ return self._reduced_dims - def set_reduced_dims( - self, reduced_dims: Dict[str, Any], in_place: bool = False - ) -> "SingleCellExperiment": + def set_reduced_dims(self, reduced_dims: Dict[str, Any], in_place: bool = False) -> "SingleCellExperiment": """Set new reduced dimensions. Args: @@ -421,9 +405,7 @@ def get_reduced_dim_names(self) -> List[str]: """ return list(self._reduced_dims.keys()) - def set_reduced_dim_names( - self, names: List[str], in_place: bool = False - ) -> "SingleCellExperiment": + def set_reduced_dim_names(self, names: List[str], in_place: bool = False) -> "SingleCellExperiment": """Replace :py:attr:`~.reduced_dims`'s names. Args: @@ -439,9 +421,7 @@ def set_reduced_dim_names( """ current_names = self.get_reduced_dim_names() if len(names) != len(current_names): - raise ValueError( - "Length of 'names' does not match the number of `reduced_dims`." - ) + raise ValueError("Length of 'names' does not match the number of `reduced_dims`.") new_reduced_dims = OrderedDict() for idx in range(len(names)): @@ -499,9 +479,7 @@ def reduced_dim(self, dimension: Union[str, int]) -> Any: return self._reduced_dims[dimension] - raise TypeError( - f"'dimension' must be a string or integer, provided '{type(dimension)}'." - ) + raise TypeError(f"'dimension' must be a string or integer, provided '{type(dimension)}'.") ################################ ######>> main_expt_name <<###### @@ -515,9 +493,7 @@ def get_main_experiment_name(self) -> Optional[str]: """ return self._main_experiment_name - def set_main_experiment_name( - self, name: Optional[str], in_place: bool = False - ) -> "SingleCellExperiment": + def set_main_experiment_name(self, name: Optional[str], in_place: bool = False) -> "SingleCellExperiment": """Set new experiment data (assays). Args: @@ -609,9 +585,7 @@ def get_alternative_experiment_names(self) -> List[str]: """ return list(self._alternative_experiments.keys()) - def set_alternative_experiment_names( - self, names: List[str], in_place: bool = False - ) -> "SingleCellExperiment": + def set_alternative_experiment_names(self, names: List[str], in_place: bool = False) -> "SingleCellExperiment": """Replace :py:attr:`~.alternative_experiment`'s names. Args: @@ -627,15 +601,11 @@ def set_alternative_experiment_names( """ current_names = self.get_alternative_experiment_names() if len(names) != len(current_names): - raise ValueError( - "Length of 'names' does not match the number of `alternative_experiments`." - ) + raise ValueError("Length of 'names' does not match the number of `alternative_experiments`.") new_alt_expts = OrderedDict() for idx in range(len(names)): - new_alt_expts[names[idx]] = self._alternative_experiments.pop( - current_names[idx] - ) + new_alt_expts[names[idx]] = self._alternative_experiments.pop(current_names[idx]) output = self._define_output(in_place) output._alternative_experiments = new_alt_expts @@ -680,13 +650,9 @@ def alternative_experiment(self, name: Union[str, int]) -> Any: raise IndexError("Index cannot be negative.") if name > len(self.alternative_experiment_names): - raise IndexError( - "Index greater than the number of alternative experiments." - ) + raise IndexError("Index greater than the number of alternative experiments.") - return self._alternative_experiments[ - self.alternative_experiment_names[name] - ] + return self._alternative_experiments[self.alternative_experiment_names[name]] elif isinstance(name, str): if name not in self._alternative_experiments: raise AttributeError(f"Alternative experiment: {name} does not exist.") @@ -707,9 +673,7 @@ def get_row_pairs(self) -> Dict[str, Any]: """ return self._row_pairs - def set_row_pairs( - self, pairs: Dict[str, Any], in_place: bool = False - ) -> "SingleCellExperiment": + def set_row_pairs(self, pairs: Dict[str, Any], in_place: bool = False) -> "SingleCellExperiment": """Replace :py:attr:`~.row_pairs`'s names. Args: @@ -755,9 +719,7 @@ def get_row_pair_names(self) -> List[str]: """ return list(self._row_pairs.keys()) - def set_row_pair_names( - self, names: List[str], in_place: bool = False - ) -> "SingleCellExperiment": + def set_row_pair_names(self, names: List[str], in_place: bool = False) -> "SingleCellExperiment": """Replace :py:attr:`~.row_pair`'s names. Args: @@ -773,9 +735,7 @@ def set_row_pair_names( """ current_names = self.get_row_pair_names() if len(names) != len(current_names): - raise ValueError( - "Length of 'names' does not match the number of `row_pairs`." - ) + raise ValueError("Length of 'names' does not match the number of `row_pairs`.") new_row_pairs = OrderedDict() for idx in range(len(names)): @@ -811,9 +771,7 @@ def get_column_pairs(self) -> Dict[str, Any]: """ return self._column_pairs - def set_column_pairs( - self, pairs: Dict[str, Any], in_place: bool = False - ) -> "SingleCellExperiment": + def set_column_pairs(self, pairs: Dict[str, Any], in_place: bool = False) -> "SingleCellExperiment": """Replace :py:attr:`~.column_pairs`'s names. Args: @@ -859,9 +817,7 @@ def get_column_pair_names(self) -> List[str]: """ return list(self._column_pairs.keys()) - def set_column_pair_names( - self, names: List[str], in_place: bool = False - ) -> "SingleCellExperiment": + def set_column_pair_names(self, names: List[str], in_place: bool = False) -> "SingleCellExperiment": """Replace :py:attr:`~.column_pair`'s names. Args: @@ -877,9 +833,7 @@ def set_column_pair_names( """ current_names = self.get_column_pair_names() if len(names) != len(current_names): - raise ValueError( - "Length of 'names' does not match the number of `column_pairs`." - ) + raise ValueError("Length of 'names' does not match the number of `column_pairs`.") new_column_pairs = OrderedDict() for idx in range(len(names)): @@ -1252,9 +1206,7 @@ def relaxed_combine_columns( _new_rdim = None try: - _new_rdim = relaxed_merge_numpy_generic( - x, by="row", attr="reduced_dims", names_attr="reduced_dim_names" - ) + _new_rdim = relaxed_merge_numpy_generic(x, by="row", attr="reduced_dims", names_attr="reduced_dim_names") except Exception as e: warn( f"Cannot combine 'reduced_dimensions' across experiments, {str(e)}", @@ -1263,9 +1215,7 @@ def relaxed_combine_columns( _new_alt_expt = None try: - _new_alt_expt = relaxed_merge_generic( - x, by="column", attr="alternative_experiments" - ) + _new_alt_expt = relaxed_merge_generic(x, by="column", attr="alternative_experiments") except Exception as e: warn( f"Cannot combine 'alternative_experiments' across experiments, {str(e)}", diff --git a/src/singlecellexperiment/io/tenx.py b/src/singlecellexperiment/io/tenx.py index 0bc74da..bb3ca44 100644 --- a/src/singlecellexperiment/io/tenx.py +++ b/src/singlecellexperiment/io/tenx.py @@ -75,9 +75,7 @@ def read_tenx_h5(path: str, realize_assays: bool = False) -> SingleCellExperimen # read the matrix shape = tuple(h5["matrix"]["shape"][:]) - counts = Hdf5CompressedSparseMatrix( - path=path, group_name="matrix", by_column=True, shape=shape - ) + counts = Hdf5CompressedSparseMatrix(path=path, group_name="matrix", by_column=True, shape=shape) if realize_assays is True: counts = da.to_scipy_sparse_matrix(counts, "csr") @@ -111,6 +109,4 @@ def read_tenx_h5(path: str, realize_assays: bool = False) -> SingleCellExperimen h5.close() - return SingleCellExperiment( - assays={"counts": counts}, row_data=features, column_data=barcodes - ) + return SingleCellExperiment(assays={"counts": counts}, row_data=features, column_data=barcodes)