From 47d2e334dc89d31fa3bd23b39dbb43b12ff841b6 Mon Sep 17 00:00:00 2001 From: Jayaram Kancherla Date: Sun, 8 Dec 2024 05:45:33 -0800 Subject: [PATCH 1/5] chore: remove Python 3.8 support --- .pre-commit-config.yaml | 25 +++++++++++++------------ CHANGELOG.md | 5 +++++ setup.cfg | 2 +- 3 files changed, 19 insertions(+), 13 deletions(-) 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 00513bb..e61a338 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.1 - 0.4.3 - Access an experiment by index. diff --git a/setup.cfg b/setup.cfg index 0873945..5c8b97e 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 From 937f674fabab4b63e7fbbe42599e2fbc4a82267e Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Sun, 8 Dec 2024 13:45:47 +0000 Subject: [PATCH 2/5] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- .../MultiAssayExperiment.py | 120 +++++------------- src/multiassayexperiment/io/interface.py | 4 +- 2 files changed, 33 insertions(+), 91 deletions(-) diff --git a/src/multiassayexperiment/MultiAssayExperiment.py b/src/multiassayexperiment/MultiAssayExperiment.py index 1fbe37e..5930494 100644 --- a/src/multiassayexperiment/MultiAssayExperiment.py +++ b/src/multiassayexperiment/MultiAssayExperiment.py @@ -55,9 +55,7 @@ def _validate_sample_map_with_column_data(sample_map, column_data): _sample_set = set(_samples) _sample_diff = _sample_set.difference(column_data.row_names) if len(_sample_diff) > 0: - raise ValueError( - "`sample_map`'s 'primary' contains samples not represented by 'row_names' from `column_data`." - ) + raise ValueError("`sample_map`'s 'primary' contains samples not represented by 'row_names' from `column_data`.") if len(_sample_set) != column_data.shape[0]: warn("'primary' from `sample_map` & `column_data` mismatch.", UserWarning) @@ -68,9 +66,7 @@ def _validate_sample_map_with_expts(sample_map, experiments): smap_unique_assays = set(sample_map.get_column("assay")) unique_expt_names = set(list(experiments.keys())) - if (len(unique_expt_names) != len(smap_unique_assays)) or ( - unique_expt_names != smap_unique_assays - ): + if (len(unique_expt_names) != len(smap_unique_assays)) or (unique_expt_names != smap_unique_assays): warn( "'experiments' contains names not represented in 'sample_map' or vice-versa.", UserWarning, @@ -86,9 +82,7 @@ def _validate_sample_map_with_expts(sample_map, experiments): ) if set(rows.get_column("colname")) != set(experiments[grp].column_names): - raise ValueError( - f"Experiment '{grp}' does not contain all columns mentioned in `sample_map`." - ) + raise ValueError(f"Experiment '{grp}' does not contain all columns mentioned in `sample_map`.") def _validate_sample_map(sample_map, column_data, experiments): @@ -99,9 +93,7 @@ def _validate_sample_map(sample_map, column_data, experiments): raise TypeError("'sample_map' is not a `BiocFrame` object.") if not set(["assay", "primary", "colname"]).issubset(sample_map.column_names): - raise ValueError( - "'sample_map' does not contain required columns: 'assay', 'primary' and 'colname'." - ) + raise ValueError("'sample_map' does not contain required columns: 'assay', 'primary' and 'colname'.") _validate_column_data(column_data) _validate_sample_map_with_column_data(sample_map, column_data) @@ -123,9 +115,7 @@ def _create_smap_from_experiments(experiments): samples.append(asy_sample) - sample_map = biocframe.BiocFrame( - {"assay": _all_assays, "primary": _all_primary, "colname": _all_colnames} - ) + sample_map = biocframe.BiocFrame({"assay": _all_assays, "primary": _all_primary, "colname": _all_colnames}) col_data = biocframe.BiocFrame({"samples": samples}, row_names=samples) return col_data, sample_map @@ -215,9 +205,7 @@ def __init__( self._column_data = _sanitize_frame(column_data) elif sample_map is None and column_data is None: # make a sample map - self._column_data, self._sample_map = _create_smap_from_experiments( - self._experiments - ) + self._column_data, self._sample_map = _create_smap_from_experiments(self._experiments) else: raise ValueError( "Either 'sample_map' or 'column_data' is `None`. Either both should be provided or set both to `None`." @@ -305,7 +293,9 @@ def __str__(self) -> str: for idx in range(len(self.experiment_names)): expt_name = self.experiment_names[idx] expt = self._experiments[expt_name] - output += f"[{idx}] {expt_name}: {type(expt).__name__} with {expt.shape[0]} rows and {expt.shape[1]} columns \n" # noqa + output += ( + f"[{idx}] {expt_name}: {type(expt).__name__} with {expt.shape[0]} rows and {expt.shape[1]} columns \n" # noqa + ) output += f"column_data columns({len(self._column_data.column_names)}): " output += f"{ut.print_truncated_list(self._column_data.column_names)}\n" @@ -331,9 +321,7 @@ def get_experiments(self) -> Dict[str, Any]: return self._experiments - def set_experiments( - self, experiments: Dict[str, Any], in_place: bool = False - ) -> "MultiAssayExperiment": + def set_experiments(self, experiments: Dict[str, Any], in_place: bool = False) -> "MultiAssayExperiment": """Set new experiments. Args: @@ -399,9 +387,7 @@ def get_experiment_names(self) -> List[str]: """ return list(self._experiments.keys()) - def set_experiment_names( - self, names: List[str], in_place: bool = False - ) -> "MultiAssayExperiment": + def set_experiment_names(self, names: List[str], in_place: bool = False) -> "MultiAssayExperiment": """Replace :py:attr:`~experiments`'s names. Args: @@ -417,9 +403,7 @@ def set_experiment_names( """ current_names = self.get_experiment_names() if len(names) != len(current_names): - raise ValueError( - "Length of 'names' does not match the number of `experiments`." - ) + raise ValueError("Length of 'names' does not match the number of `experiments`.") new_experiments = OrderedDict() for idx in range(len(names)): @@ -490,9 +474,7 @@ def experiment(self, name: Union[int, str], with_sample_data: bool = False) -> A expt = self.experiments[name] else: - raise TypeError( - f"'experiment' must be a string or integer, provided '{type(name)}'." - ) + raise TypeError(f"'experiment' must be a string or integer, provided '{type(name)}'.") if with_sample_data is True: assay_splits = self.sample_map.split("assay", only_indices=True) @@ -500,13 +482,9 @@ def experiment(self, name: Union[int, str], with_sample_data: bool = False) -> A subset_map = subset_map.set_row_names(subset_map.get_column("colname")) expt_column_data = expt.column_data - new_column_data = biocframe.merge( - [subset_map, expt_column_data], join="outer" - ) + new_column_data = biocframe.merge([subset_map, expt_column_data], join="outer") - new_column_data = biocframe.merge( - [new_column_data, self._column_data], join="left" - ) + new_column_data = biocframe.merge([new_column_data, self._column_data], join="left") return expt.set_column_data(new_column_data, in_place=False) @@ -531,9 +509,7 @@ def get_sample_map(self) -> biocframe.BiocFrame: """ return self._sample_map - def set_sample_map( - self, sample_map: biocframe.BiocFrame, in_place: bool = False - ) -> "MultiAssayExperiment": + def set_sample_map(self, sample_map: biocframe.BiocFrame, in_place: bool = False) -> "MultiAssayExperiment": """Set new sample mapping. Args: @@ -583,9 +559,7 @@ def get_column_data(self) -> biocframe.BiocFrame: """ return self._column_data - def set_column_data( - self, column_data: biocframe.BiocFrame, in_place: bool = False - ) -> "MultiAssayExperiment": + def set_column_data(self, column_data: biocframe.BiocFrame, in_place: bool = False) -> "MultiAssayExperiment": """Set new sample metadata. Args: @@ -636,9 +610,7 @@ def get_metadata(self) -> dict: """ return self._metadata - def set_metadata( - self, metadata: dict, in_place: bool = False - ) -> "MultiAssayExperiment": + def set_metadata(self, metadata: dict, in_place: bool = False) -> "MultiAssayExperiment": """Set additional metadata. Args: @@ -653,9 +625,7 @@ def set_metadata( or as a reference to the (in-place-modified) original. """ if not isinstance(metadata, dict): - raise TypeError( - f"`metadata` must be a dictionary, provided {type(metadata)}." - ) + raise TypeError(f"`metadata` must be a dictionary, provided {type(metadata)}.") output = self._define_output(in_place) output._metadata = metadata return output @@ -684,9 +654,7 @@ def metadata(self, metadata: dict): def _normalize_column_slice(self, columns: Union[str, int, bool, Sequence, slice]): _scalar = None if columns != slice(None): - columns, _scalar = ut.normalize_subscript( - columns, len(self._column_data), self._column_data.row_names - ) + columns, _scalar = ut.normalize_subscript(columns, len(self._column_data), self._column_data.row_names) return columns, _scalar @@ -744,9 +712,7 @@ def subset_experiments( experiment_names = slice(None) if experiment_names != slice(None): - expts, _ = ut.normalize_subscript( - experiment_names, len(self.experiment_names), self.experiment_names - ) + expts, _ = ut.normalize_subscript(experiment_names, len(self.experiment_names), self.experiment_names) to_keep = [self.experiment_names[idx] for idx in expts] @@ -824,9 +790,7 @@ def _generic_slice( if experiments is None: experiments = slice(None) - _new_experiments = self.subset_experiments( - experiment_names=experiments, rows=rows, columns=columns - ) + _new_experiments = self.subset_experiments(experiment_names=experiments, rows=rows, columns=columns) # filter sample_map smap_indices_to_keep = [] @@ -844,9 +808,7 @@ def _generic_slice( return SlicerResult(_new_experiments, _new_sample_map, _new_column_data) - def subset_by_experiments( - self, experiments: Union[str, int, bool, Sequence] - ) -> "MultiAssayExperiment": + def subset_by_experiments(self, experiments: Union[str, int, bool, Sequence]) -> "MultiAssayExperiment": """Subset by experiment(s). Args: @@ -863,13 +825,9 @@ def subset_by_experiments( A new `MultiAssayExperiment` with the subset experiments. """ sresult = self._generic_slice(experiments=experiments) - return MultiAssayExperiment( - sresult.experiments, sresult.column_data, sresult.sample_map, self.metadata - ) + return MultiAssayExperiment(sresult.experiments, sresult.column_data, sresult.sample_map, self.metadata) - def subset_by_row( - self, rows: Union[str, int, bool, Sequence] - ) -> "MultiAssayExperiment": + def subset_by_row(self, rows: Union[str, int, bool, Sequence]) -> "MultiAssayExperiment": """Subset by rows. Args: @@ -884,13 +842,9 @@ def subset_by_row( A new `MultiAssayExperiment` with the subsetted rows. """ sresult = self._generic_slice(rows=rows) - return MultiAssayExperiment( - sresult.experiments, sresult.column_data, sresult.sample_map, self.metadata - ) + return MultiAssayExperiment(sresult.experiments, sresult.column_data, sresult.sample_map, self.metadata) - def subset_by_column( - self, columns: Union[str, int, bool, Sequence] - ) -> "MultiAssayExperiment": + def subset_by_column(self, columns: Union[str, int, bool, Sequence]) -> "MultiAssayExperiment": """Subset by column. Args: @@ -905,9 +859,7 @@ def subset_by_column( A new `MultiAssayExperiment` with the subsetted columns. """ sresult = self._generic_slice(columns=columns) - return MultiAssayExperiment( - sresult.experiments, sresult.column_data, sresult.sample_map, self.metadata - ) + return MultiAssayExperiment(sresult.experiments, sresult.column_data, sresult.sample_map, self.metadata) def __getitem__(self, args: tuple) -> "MultiAssayExperiment": """Subset a `MultiAssayExperiment`. @@ -948,9 +900,7 @@ def __getitem__(self, args: tuple) -> "MultiAssayExperiment": self.metadata, ) elif len(args) == 3: - sresult = self._generic_slice( - rows=args[0], columns=args[1], experiments=args[2] - ) + sresult = self._generic_slice(rows=args[0], columns=args[1], experiments=args[2]) return MultiAssayExperiment( sresult.experiments, sresult.column_data, @@ -1250,9 +1200,7 @@ def from_mudata(cls, input: "mudata.MuData") -> "MultiAssayExperiment": samples.append(asy_sample) - sample_map = biocframe.BiocFrame( - {"assay": _all_assays, "primary": _all_primary, "colname": _all_colnames} - ) + sample_map = biocframe.BiocFrame({"assay": _all_assays, "primary": _all_primary, "colname": _all_colnames}) col_data = biocframe.BiocFrame({"samples": samples}, row_names=samples) return cls( @@ -1263,9 +1211,7 @@ def from_mudata(cls, input: "mudata.MuData") -> "MultiAssayExperiment": ) @classmethod - def from_anndata( - cls, input: "anndata.AnnData", name: str = "unknown" - ) -> "MultiAssayExperiment": + def from_anndata(cls, input: "anndata.AnnData", name: str = "unknown") -> "MultiAssayExperiment": """Create a ``MultiAssayExperiment`` from :py:class:`~anndata.AnnData`. Since :py:class:`~anndata.AnnData` does not contain sample information, @@ -1292,9 +1238,7 @@ def from_anndata( experiments = {name: scexpt} - col_data = biocframe.BiocFrame( - {"samples": ["unknown_sample"]}, row_names=["unknown_sample"] - ) + col_data = biocframe.BiocFrame({"samples": ["unknown_sample"]}, row_names=["unknown_sample"]) colnames = None diff --git a/src/multiassayexperiment/io/interface.py b/src/multiassayexperiment/io/interface.py index ab68799..f1dfec3 100644 --- a/src/multiassayexperiment/io/interface.py +++ b/src/multiassayexperiment/io/interface.py @@ -46,9 +46,7 @@ def make_mae(experiments: Dict[str, Any]) -> MultiAssayExperiment: failedExpts = [] for expname, expt in experiments.items(): - if not ( - isinstance(expt, AnnData) or issubclass(type(expt), SummarizedExperiment) - ): + if not (isinstance(expt, AnnData) or issubclass(type(expt), SummarizedExperiment)): failedExpts.append(expname) if len(failedExpts) > 0: From 798e6cbdcc3c42f287447f89b13e72ebb5ede967 Mon Sep 17 00:00:00 2001 From: Jayaram Kancherla Date: Sun, 8 Dec 2024 06:31:44 -0800 Subject: [PATCH 3/5] Add tests for Python 3.13 --- .github/workflows/pypi-publish.yml | 4 ++-- .github/workflows/pypi-test.yml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/pypi-publish.yml b/.github/workflows/pypi-publish.yml index 7b591a2..cc1d1fb 100644 --- a/.github/workflows/pypi-publish.yml +++ b/.github/workflows/pypi-publish.yml @@ -14,10 +14,10 @@ jobs: steps: - uses: actions/checkout@v2 - - name: Set up Python 3.9 + - name: Set up Python 3.11 uses: actions/setup-python@v2 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..31b7875 100644 --- a/.github/workflows/pypi-test.yml +++ b/.github/workflows/pypi-test.yml @@ -15,7 +15,7 @@ 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: From 2360677833cc117198259c191e562da46cd8364e Mon Sep 17 00:00:00 2001 From: Jayaram Kancherla Date: Sun, 8 Dec 2024 06:54:57 -0800 Subject: [PATCH 4/5] bump action versions --- .github/workflows/pypi-publish.yml | 4 ++-- .github/workflows/pypi-test.yml | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/pypi-publish.yml b/.github/workflows/pypi-publish.yml index cc1d1fb..030cd10 100644 --- a/.github/workflows/pypi-publish.yml +++ b/.github/workflows/pypi-publish.yml @@ -13,9 +13,9 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 - name: Set up Python 3.11 - uses: actions/setup-python@v2 + uses: actions/setup-python@v5 with: python-version: 3.11 - name: Install dependencies diff --git a/.github/workflows/pypi-test.yml b/.github/workflows/pypi-test.yml index 31b7875..22f6c4a 100644 --- a/.github/workflows/pypi-test.yml +++ b/.github/workflows/pypi-test.yml @@ -19,9 +19,9 @@ jobs: 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' From d74d1ad4aceda91d8ac9d926bdc1a55a72440520 Mon Sep 17 00:00:00 2001 From: Jayaram Kancherla Date: Fri, 20 Dec 2024 11:55:40 -0800 Subject: [PATCH 5/5] minor changes --- .github/workflows/pypi-publish.yml | 75 +++++++++++++++--------------- .github/workflows/pypi-test.yml | 47 ++++++++----------- pyproject.toml | 4 ++ 3 files changed, 62 insertions(+), 64 deletions(-) diff --git a/.github/workflows/pypi-publish.yml b/.github/workflows/pypi-publish.yml index 030cd10..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@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 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 22f6c4a..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.9', '3.10', '3.11', '3.12', '3.13' ] + python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"] name: Python ${{ matrix.python-version }} steps: - - 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 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/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"]