diff --git a/.coveragerc b/.coveragerc index 567d183..7235976 100644 --- a/.coveragerc +++ b/.coveragerc @@ -1,8 +1,8 @@ # .coveragerc to control coverage.py [run] branch = True -source = summarizedexperiment -# 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/pre-commit.yml b/.github/workflows/pre-commit.yml new file mode 100644 index 0000000..e940068 --- /dev/null +++ b/.github/workflows/pre-commit.yml @@ -0,0 +1,16 @@ +name: pre-commit + +on: + pull_request: + push: + branches: [main] + +jobs: + pre-commit: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v7 + - uses: actions/setup-python@v5 + with: + python-version: '3.12' + - uses: pre-commit/action@v3.0.1 diff --git a/.github/workflows/publish-pypi.yml b/.github/workflows/publish-pypi.yml index 405fee0..7847cdf 100644 --- a/.github/workflows/publish-pypi.yml +++ b/.github/workflows/publish-pypi.yml @@ -1,52 +1,91 @@ -name: Publish to PyPI +name: Publish to PyPI and GitHub Pages on: push: tags: "*" jobs: - build: + build-and-test: + name: Build and Test runs-on: ubuntu-latest - permissions: - id-token: write - repository-projects: write - contents: write - pages: write - steps: - - 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 dependencies - run: | - python -m pip install --upgrade pip - pip install tox + - name: Install tox + run: python -m pip install tox - - name: Test with tox - run: | - tox + - name: Test + run: tox -e default - - name: Build Project and Publish - run: | - python -m tox -e clean,build + - name: Build Project + run: tox -e build - # This uses the trusted publisher workflow so no token is required. - - name: Publish to PyPI - uses: pypa/gh-action-pypi-publish@release/v1 + - name: Store the distribution packages + uses: actions/upload-artifact@v4 + with: + name: python-package-distributions + path: dist/ + + build-docs: + name: Build Documentation + runs-on: ubuntu-latest + steps: + - 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 docs - run: | - tox -e docs + run: tox -e docs - - run: touch ./docs/_build/html/.nojekyll + - name: Add .nojekyll + run: touch ./docs/_build/html/.nojekyll - - name: GH Pages Deployment - uses: JamesIves/github-pages-deploy-action@v4 + - name: Upload Pages artifact + uses: actions/upload-pages-artifact@v5 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 + path: ./docs/_build/html + + publish-pypi: + name: Publish to PyPI + needs: build-and-test + runs-on: ubuntu-latest + environment: + name: pypi + url: https://pypi.org/p/summarizedexperiment + permissions: + id-token: write # IMPORTANT: mandatory for trusted publishing + steps: + - name: Download all the dists + uses: actions/download-artifact@v8 + with: + 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 3d3bc95..ca79fe4 100644 --- a/.github/workflows/run-tests.yml +++ b/.github/workflows/run-tests.yml @@ -36,21 +36,18 @@ jobs: 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: 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 @@ -65,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/.gitignore b/.gitignore index 19d4b9b..bbf3ba0 100644 --- a/.gitignore +++ b/.gitignore @@ -51,3 +51,59 @@ MANIFEST # Per-project virtualenvs .venv*/ .conda*/ + +# Byte-compiled / optimized / DLL files +__pycache__/ +*$py.class +# C extensions +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +# PyInstaller +*.manifest +*.spec +# Installer logs +pip-log.txt +pip-delete-this-directory.txt +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.cache +nosetests.xml +*.cover +*.py,cover +.hypothesis/ +cover/ +# Sphinx documentation +docs/_build/ +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ +# mypy, ruff, etc +.mypy_cache/ +.ruff_cache/ +.pyre/ +# Editors +.vscode/ +.idea/ +*.swp +*.swo 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/CHANGELOG.md b/CHANGELOG.md index 0ba8bfa..e17f317 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # Changelog +## Version 0.8.0 + +- Migrate to hatch. + ## Version 0.7.0 - Extended `__getitem__` subscripting on `RangedSummarizedExperiment` to support direct slicing by `GenomicRanges` or `CompressedGenomicRangesList` objects. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 1fbbae0..02b4546 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,371 +1,20 @@ -```{todo} THIS IS SUPPOSED TO BE AN EXAMPLE. MODIFY IT ACCORDING TO YOUR NEEDS! - - The document assumes you are using a source repository service that promotes a - contribution model similar to [GitHub's fork and pull request workflow]. - While this is true for the majority of services (like GitHub, GitLab, - BitBucket), it might not be the case for private repositories (e.g., when - using Gerrit). - - Also notice that the code examples might refer to GitHub URLs or the text - might use GitHub specific terminology (e.g., *Pull Request* instead of *Merge - Request*). - - Please make sure to check the document having these assumptions in mind - and update things accordingly. -``` - -```{todo} Provide the correct links/replacements at the bottom of the document. -``` - -```{todo} You might want to have a look on [PyScaffold's contributor's guide], - - especially if your project is open source. The text should be very similar to - this template, but there are a few extra contents that you might decide to - also include, like mentioning labels of your issue tracker or automated - releases. -``` - # Contributing -Welcome to `SummarizedExperiment` contributor's guide. - -This document focuses on getting any potential contributor familiarized with -the development processes, but [other kinds of contributions] are also appreciated. - -If you are new to using [git] or have never collaborated in a project previously, -please have a look at [contribution-guide.org]. Other resources are also -listed in the excellent [guide created by FreeCodeCamp] [^contrib1]. - -Please notice, all users and contributors are expected to be **open, -considerate, reasonable, and respectful**. When in doubt, -[Python Software Foundation's Code of Conduct] is a good reference in terms of -behavior guidelines. - -## Issue Reports - -If you experience bugs or general issues with `SummarizedExperiment`, please have a look -on the [issue tracker]. -If you don't see anything useful there, please feel free to fire an issue report. - -:::{tip} -Please don't forget to include the closed issues in your search. -Sometimes a solution was already reported, and the problem is considered -**solved**. -::: - -New issue reports should include information about your programming environment -(e.g., operating system, Python version) and steps to reproduce the problem. -Please try also to simplify the reproduction steps to a very minimal example -that still illustrates the problem you are facing. By removing other factors, -you help us to identify the root cause of the issue. - -## Documentation Improvements - -You can help improve `SummarizedExperiment` docs by making them more readable and coherent, or -by adding missing information and correcting mistakes. - -`SummarizedExperiment` documentation uses [Sphinx] as its main documentation compiler. -This means that the docs are kept in the same repository as the project code, and -that any documentation update is done in the same way was a code contribution. - -```{todo} Don't forget to mention which markup language you are using. - - e.g., [reStructuredText] or [CommonMark] with [MyST] extensions. -``` - -```{todo} If your project is hosted on GitHub, you can also mention the following tip: - - :::{tip} - Please notice that the [GitHub web interface] provides a quick way of - propose changes in `SummarizedExperiment`'s files. While this mechanism can - be tricky for normal code contributions, it works perfectly fine for - contributing to the docs, and can be quite handy. - - If you are interested in trying this method out, please navigate to - the `docs` folder in the source [repository], find which file you - would like to propose changes and click in the little pencil icon at the - top, to open [GitHub's code editor]. Once you finish editing the file, - please write a message in the form at the bottom of the page describing - which changes have you made and what are the motivations behind them and - submit your proposal. - ::: -``` - -When working on documentation changes in your local machine, you can -compile them using [tox] : - -``` -tox -e docs -``` - -and use Python's built-in web server for a preview in your web browser -(`http://localhost:8000`): - -``` -python3 -m http.server --directory 'docs/_build/html' -``` - -## Code Contributions - -```{todo} Please include a reference or explanation about the internals of the project. - - An architecture description, design principles or at least a summary of the - main concepts will make it easy for potential contributors to get started - quickly. -``` - -### Submit an issue - -Before you work on any non-trivial code contribution it's best to first create -a report in the [issue tracker] to start a discussion on the subject. -This often provides additional considerations and avoids unnecessary work. - -### Create an environment - -Before you start coding, we recommend creating an isolated [virtual environment] -to avoid any problems with your installed Python packages. -This can easily be done via either [virtualenv]: - -``` -virtualenv -source /bin/activate -``` - -or [Miniconda]: - -``` -conda create -n SummarizedExperiment python=3 six virtualenv pytest pytest-cov -conda activate SummarizedExperiment -``` - -### Clone the repository - -1. Create an user account on GitHub if you do not already have one. - -2. Fork the project [repository]: click on the *Fork* button near the top of the - page. This creates a copy of the code under your account on GitHub. - -3. Clone this copy to your local disk: - - ``` - git clone git@github.com:YourLogin/SummarizedExperiment.git - cd SummarizedExperiment - ``` - -4. You should run: - - ``` - pip install -U pip setuptools -e . - ``` - - to be able to import the package under development in the Python REPL. - - ```{todo} if you are not using pre-commit, please remove the following item: - ``` - -5. Install [pre-commit]: - - ``` - pip install pre-commit - pre-commit install - ``` - - `SummarizedExperiment` comes with a lot of hooks configured to automatically help the - developer to check the code being written. - -### Implement your changes - -1. Create a branch to hold your changes: - - ``` - git checkout -b my-feature - ``` - - and start making changes. Never work on the main branch! - -2. Start your work on this branch. Don't forget to add [docstrings] to new - functions, modules and classes, especially if they are part of public APIs. - -3. Add yourself to the list of contributors in `AUTHORS.rst`. - -4. When you’re done editing, do: - - ``` - git add - git commit - ``` - - to record your changes in [git]. - - ```{todo} if you are not using pre-commit, please remove the following item: - ``` - - Please make sure to see the validation messages from [pre-commit] and fix - any eventual issues. - This should automatically use [flake8]/[black] to check/fix the code style - in a way that is compatible with the project. - - :::{important} - Don't forget to add unit tests and documentation in case your - contribution adds an additional feature and is not just a bugfix. - - Moreover, writing a [descriptive commit message] is highly recommended. - In case of doubt, you can check the commit history with: - - ``` - git log --graph --decorate --pretty=oneline --abbrev-commit --all - ``` - - to look for recurring communication patterns. - ::: - -5. Please check that your changes don't break any unit tests with: - - ``` - tox - ``` - - (after having installed [tox] with `pip install tox` or `pipx`). - - You can also use [tox] to run several other pre-configured tasks in the - repository. Try `tox -av` to see a list of the available checks. - -### Submit your contribution - -1. If everything works fine, push your local branch to the remote server with: - - ``` - git push -u origin my-feature - ``` - -2. Go to the web page of your fork and click "Create pull request" - to send your changes for review. - - ```{todo} if you are using GitHub, you can uncomment the following paragraph - - Find more detailed information in [creating a PR]. You might also want to open - the PR as a draft first and mark it as ready for review after the feedbacks - from the continuous integration (CI) system or any required fixes. - - ``` - -### Troubleshooting - -The following tips can be used when facing problems to build or test the -package: - -1. Make sure to fetch all the tags from the upstream [repository]. - The command `git describe --abbrev=0 --tags` should return the version you - are expecting. If you are trying to run CI scripts in a fork repository, - make sure to push all the tags. - You can also try to remove all the egg files or the complete egg folder, i.e., - `.eggs`, as well as the `*.egg-info` folders in the `src` folder or - potentially in the root of your project. - -2. Sometimes [tox] misses out when new dependencies are added, especially to - `setup.cfg` and `docs/requirements.txt`. If you find any problems with - missing dependencies when running a command with [tox], try to recreate the - `tox` environment using the `-r` flag. For example, instead of: - - ``` - tox -e docs - ``` - - Try running: - - ``` - tox -r -e docs - ``` - -3. Make sure to have a reliable [tox] installation that uses the correct - Python version (e.g., 3.7+). When in doubt you can run: - - ``` - tox --version - # OR - which tox - ``` - - If you have trouble and are seeing weird errors upon running [tox], you can - also try to create a dedicated [virtual environment] with a [tox] binary - freshly installed. For example: - - ``` - virtualenv .venv - source .venv/bin/activate - .venv/bin/pip install tox - .venv/bin/tox -e all - ``` - -4. [Pytest can drop you] in an interactive session in the case an error occurs. - In order to do that you need to pass a `--pdb` option (for example by - running `tox -- -k --pdb`). - You can also setup breakpoints manually instead of using the `--pdb` option. - -## Maintainer tasks - -### Releases - -```{todo} This section assumes you are using PyPI to publicly release your package. - - If instead you are using a different/private package index, please update - the instructions accordingly. -``` - -If you are part of the group of maintainers and have correct user permissions -on [PyPI], the following steps can be used to release a new version for -`SummarizedExperiment`: - -1. Make sure all unit tests are successful. -2. Tag the current commit on the main branch with a release tag, e.g., `v1.2.3`. -3. Push the new tag to the upstream [repository], - e.g., `git push upstream v1.2.3` -4. Clean up the `dist` and `build` folders with `tox -e clean` - (or `rm -rf dist build`) - to avoid confusion with old builds and Sphinx docs. -5. Run `tox -e build` and check that the files in `dist` have - the correct version (no `.dirty` or [git] hash) according to the [git] tag. - Also check the sizes of the distributions, if they are too big (e.g., > - 500KB), unwanted clutter may have been accidentally included. -6. Run `tox -e publish -- --repository pypi` and check that everything was - uploaded to [PyPI] correctly. - -[^contrib1]: Even though, these resources focus on open source projects and - communities, the general ideas behind collaborating with other developers - to collectively create software are general and can be applied to all sorts - of environments, including private companies and proprietary code bases. +Contributions are welcome, and they are greatly appreciated! Every little bit helps, and credit will always be given. +## Report Bugs +Report bugs at the issue tracker. -[black]: https://pypi.org/project/black/ -[commonmark]: https://commonmark.org/ -[contribution-guide.org]: http://www.contribution-guide.org/ -[creating a pr]: https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/proposing-changes-to-your-work-with-pull-requests/creating-a-pull-request -[descriptive commit message]: https://chris.beams.io/posts/git-commit -[docstrings]: https://www.sphinx-doc.org/en/master/usage/extensions/napoleon.html -[first-contributions tutorial]: https://github.com/firstcontributions/first-contributions -[flake8]: https://flake8.pycqa.org/en/stable/ -[git]: https://git-scm.com -[github web interface]: https://docs.github.com/en/github/managing-files-in-a-repository/managing-files-on-github/editing-files-in-your-repository -[github's code editor]: https://docs.github.com/en/github/managing-files-in-a-repository/managing-files-on-github/editing-files-in-your-repository -[github's fork and pull request workflow]: https://guides.github.com/activities/forking/ -[guide created by freecodecamp]: https://github.com/freecodecamp/how-to-contribute-to-open-source -[miniconda]: https://docs.conda.io/en/latest/miniconda.html -[myst]: https://myst-parser.readthedocs.io/en/latest/syntax/syntax.html -[other kinds of contributions]: https://opensource.guide/how-to-contribute -[pre-commit]: https://pre-commit.com/ -[pypi]: https://pypi.org/ -[pyscaffold's contributor's guide]: https://pyscaffold.org/en/stable/contributing.html -[pytest can drop you]: https://docs.pytest.org/en/stable/usage.html#dropping-to-pdb-python-debugger-at-the-start-of-a-test -[python software foundation's code of conduct]: https://www.python.org/psf/conduct/ -[restructuredtext]: https://www.sphinx-doc.org/en/master/usage/restructuredtext/ -[sphinx]: https://www.sphinx-doc.org/en/master/ -[tox]: https://tox.readthedocs.io/en/stable/ -[virtual environment]: https://realpython.com/python-virtual-environments-a-primer/ -[virtualenv]: https://virtualenv.pypa.io/en/stable/ +## Fix Bugs +Look through the GitHub issues for bugs. Anything tagged with "bug" and "help wanted" is open to whoever wants to implement it. +## Implement Features +Look through the GitHub issues for features. Anything tagged with "enhancement" and "help wanted" is open to whoever wants to implement it. -```{todo} Please review and change the following definitions: -``` +## Submit Feedback +The best way to send feedback is to file an issue. -[repository]: https://github.com//SummarizedExperiment -[issue tracker]: https://github.com//SummarizedExperiment/issues +If you are proposing a feature: +- Explain in detail how it would work. +- Keep the scope as narrow as possible, to make it easier to implement. +- Remember that this is a volunteer-driven project, and that contributions are welcome! diff --git a/docs/conf.py b/docs/conf.py index 4251a19..bdc2a7b 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -72,7 +72,6 @@ "sphinx.ext.ifconfig", "sphinx.ext.mathjax", "sphinx.ext.napoleon", - "sphinx_autodoc_typehints", ] # Add any paths that contain templates here, relative to this directory. @@ -80,8 +79,7 @@ # Enable markdown -# extensions.append("myst_parser") -extensions.append("myst_nb") +extensions.append("myst_parser") # Configure MyST-Parser myst_enable_extensions = [ @@ -107,8 +105,8 @@ master_doc = "index" # General information about the project. -project = "SummarizedExperiment" -copyright = "2023, jkanche" +project = "summarizedexperiment" +copyright = "2026, Jayaram Kancherla" # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the @@ -119,9 +117,10 @@ # If you don’t need the separation provided between version and release, # just set them both to the same value. try: - from summarizedexperiment import __version__ as version -except ImportError: - version = "" + from importlib.metadata import version as get_version + version = get_version("summarizedexperiment") +except Exception: + version = "unknown" if not version or version.lower() == "unknown": version = os.getenv("READTHEDOCS_VERSION", "unknown") # automatically set by RTD @@ -168,28 +167,29 @@ # If this is True, todo emits a warning for each TODO entries. The default is False. todo_emit_warnings = True -autodoc_default_options = { - # 'members': 'var1, var2', - # 'member-order': 'bysource', - "special-members": True, - "undoc-members": True, - "exclude-members": "__weakref__, __dict__, __str__, __module__", -} - -autosummary_generate = True -autosummary_imported_members = True - # -- Options for HTML output ------------------------------------------------- # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. -html_theme = "furo" +html_theme = "alabaster" # Theme options are theme-specific and customize the look and feel of a theme # further. For a list of options available for each theme, see the # documentation. -html_theme_options = {"sidebar_width": "300px", "page_width": "1200px"} +html_theme_options = { + "light_css_variables": { + "color-brand-primary": "#0052cc", + "color-brand-content": "#0052cc", + }, + "dark_css_variables": { + "color-brand-primary": "#4c9aff", + "color-brand-content": "#4c9aff", + }, + "source_repository": "https://github.com/biocpy/summarizedexperiment", + "source_branch": "main", + "source_directory": "docs/", +} # Add any paths that contain custom themes here, relative to this directory. # html_theme_path = [] @@ -257,7 +257,7 @@ # html_file_suffix = None # Output file base name for HTML help builder. -htmlhelp_basename = "SummarizedExperiment-doc" +htmlhelp_basename = "summarizedexperiment-doc" # -- Options for LaTeX output ------------------------------------------------ @@ -274,13 +274,7 @@ # Grouping the document tree into LaTeX files. List of tuples # (source start file, target name, title, author, documentclass [howto/manual]). latex_documents = [ - ( - "index", - "user_guide.tex", - "SummarizedExperiment Documentation", - "jkanche", - "manual", - ) + ("index", "user_guide.tex", "summarizedexperiment Documentation", "Jayaram Kancherla", "manual") ] # The name of an image file (relative to this directory) to place at the top of @@ -321,3 +315,23 @@ } print(f"loading configurations for {project} {version} ...", file=sys.stderr) + +# -- Biocsetup configuration ------------------------------------------------- + +# Enable execution of code chunks in markdown +extensions.remove('myst_parser') +extensions.append('myst_nb') + +# Less verbose api documentation +extensions.append('sphinx_autodoc_typehints') + +autodoc_default_options = { + "special-members": True, + "undoc-members": True, + "exclude-members": "__weakref__, __dict__, __str__, __module__", +} + +autosummary_generate = True +autosummary_imported_members = True + +html_theme = "furo" diff --git a/pyproject.toml b/pyproject.toml index 00aa968..ef1cc8f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,27 +1,120 @@ +[project] +name = "summarizedexperiment" +dynamic = [ + "version", +] +description = "Container to represent data from genomic experiments" +readme = "README.md" +authors = [ + { name = "Jayaram Kancherla", email = "jayaram.kancherla@gmail.com" }, +] +requires-python = ">=3.10" +keywords = [ + "bioinformatics", + "computational biology", + "genomics", + "bioconductor", + "BiocPy", + "SummarizedExperiment", + "transcriptomics", + "single-cell", +] +classifiers = [ + "Development Status :: 4 - Beta", + "Intended Audience :: Science/Research", + "Intended Audience :: Developers", + "License :: OSI Approved :: MIT License", + "Operating System :: OS Independent", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", + "Programming Language :: Python :: 3.14", + "Topic :: Scientific/Engineering", + "Topic :: Scientific/Engineering :: Bio-Informatics", + "Typing :: Typed", +] +dependencies = [ + "biocframe>=0.7.2", + "biocutils>=0.3.3", + "genomicranges>=0.8.2", + "importlib-metadata>=9.0.0 ; python_full_version < '3.8'", +] + + +[project.license] +file = "LICENSE.txt" + + +[project.urls] +Homepage = "https://github.com/BiocPy/summarizedexperiment" +Documentation = "https://biocpy.github.io/summarizedexperiment/" +Source = "https://github.com/BiocPy/summarizedexperiment" +"Bug Tracker" = "https://github.com/BiocPy/summarizedexperiment/issues" + +[project.optional-dependencies] +optional = [ + "anndata", + "delayedarray", + "scipy", +] +testing = [ + "anndata", + "delayedarray", + "pytest", + "pytest-cov", +] + [build-system] -# AVOID CHANGING REQUIRES: IT WILL BE UPDATED BY PYSCAFFOLD! -requires = ["setuptools>=46.1.0", "setuptools_scm[toml]>=5", "wheel"] -build-backend = "setuptools.build_meta" +requires = [ + "hatchling", + "hatch-vcs", +] +build-backend = "hatchling.build" -[tool.setuptools_scm] -# See configuration details in https://github.com/pypa/setuptools_scm -version_scheme = "no-guess-dev" +[tool.hatch.version] +source = "vcs" +fallback-version = "0.1.0" [tool.ruff] line-length = 120 -src = ["src"] -exclude = ["tests"] -extend-ignore = ["F821"] +src = [ + "src", +] +exclude = [ + "tests", + "docs", +] + +[tool.ruff.lint] +extend-ignore = [ + "F821", +] -[tool.ruff.pydocstyle] +[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.mypy] +strict = true + +[tool.pytest.ini_options] +addopts = "--cov --cov-report term-missing" +testpaths = [ + "tests", +] -[tool.black] -force-exclude = "__init__.py" +[tool.bandit] +exclude_dirs = ["tests"] +skips = ["B110"] diff --git a/setup.cfg b/setup.cfg deleted file mode 100644 index bd56db4..0000000 --- a/setup.cfg +++ /dev/null @@ -1,134 +0,0 @@ -# This file is used to configure your project. -# Read more about the various options under: -# https://setuptools.pypa.io/en/latest/userguide/declarative_config.html -# https://setuptools.pypa.io/en/latest/references/keywords.html - -[metadata] -name = SummarizedExperiment -description = Container to represent data from genomic experiments -author = jkanche -author_email = jayaram.kancherla@gmail.com -license = MIT -license_files = LICENSE.txt -long_description = file: README.md -long_description_content_type = text/markdown; charset=UTF-8; variant=GFM -url = https://github.com/BiocPy/summarizedexperiment -# Add here related links, for example: -project_urls = - Documentation = https://biocpy.github.io/SummarizedExperiment/ - Source = https://github.com/BiocPy/summarizedexperiment -# Changelog = https://pyscaffold.org/en/latest/changelog.html -# Tracker = https://github.com/pyscaffold/pyscaffold/issues -# Conda-Forge = https://anaconda.org/conda-forge/pyscaffold -# Download = https://pypi.org/project/PyScaffold/#files -# Twitter = https://twitter.com/PyScaffold - -# Change if running only on Windows, Mac or Linux (comma-separated) -platforms = any - -# Add here all kinds of additional classifiers as defined under -# https://pypi.org/classifiers/ -classifiers = - Development Status :: 4 - Beta - Programming Language :: Python - - -[options] -zip_safe = False -packages = find_namespace: -include_package_data = True -package_dir = - =src - -# Require a min/specific Python version (comma-separated conditions) -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 -# new major versions. This works if the required packages follow Semantic Versioning. -# For more information, check out https://semver.org/. -install_requires = - importlib-metadata; python_version<"3.8" - genomicranges>=0.8.2 - biocframe>=0.7.2 - biocutils>=0.3.3 - -[options.packages.find] -where = src -exclude = - tests - -[options.extras_require] -# Add here additional requirements for extra features, to install with: -# `pip install SummarizedExperiment[PDF]` like: -optional = - anndata - scipy - delayedarray - -# Add here test requirements (semicolon/line-separated) -testing = - setuptools - pytest - pytest-cov - anndata - delayedarray - -[options.entry_points] -# Add here console scripts like: -# console_scripts = -# script_name = summarizedexperiment.module:function -# For example: -# console_scripts = -# fibonacci = summarizedexperiment.skeleton:run -# And any other entry points, for example: -# pyscaffold.cli = -# awesome = pyscaffoldext.awesome.extension:AwesomeExtension - -[tool:pytest] -# Specify command line options as you would do when invoking pytest directly. -# e.g. --cov-report html (or xml) for html/xml output or --junitxml junit.xml -# in order to write a coverage file that can be read by Jenkins. -# CAUTION: --cov flags may prohibit setting breakpoints while debugging. -# Comment those flags to avoid this pytest issue. -addopts = - --cov summarizedexperiment --cov-report term-missing - --verbose -norecursedirs = - dist - build - .tox -testpaths = tests -# Use pytest markers to select/deselect specific tests -# markers = -# slow: mark tests as slow (deselect with '-m "not slow"') -# system: mark end-to-end system tests - -[devpi:upload] -# Options for the devpi: PyPI server and packaging tool -# VCS export must be deactivated since we are using setuptools-scm -no_vcs = 1 -formats = bdist_wheel - -[flake8] -# Some sane defaults for the code style checker flake8 -max_line_length = 100 -extend_ignore = E203, W503 -# ^ Black-compatible -# E203 and W503 have edge cases handled by black -exclude = - .tox - build - dist - .eggs - docs/conf.py -per-file-ignores = __init__.py:F401 - -[pyscaffold] -# PyScaffold's parameters when the project was created. -# This will be used when updating. Do not change! -version = 4.5 -package = summarizedexperiment -extensions = - markdown - pre_commit diff --git a/setup.py b/setup.py deleted file mode 100644 index cd7a07d..0000000 --- a/setup.py +++ /dev/null @@ -1,20 +0,0 @@ -"""Setup file for SummarizedExperiment. Use setup.cfg to configure your project. - -This file was generated with PyScaffold 4.5. -PyScaffold helps you to put up the scaffold of your new Python project. -Learn more under: https://pyscaffold.org/ -""" - -from setuptools import setup - -if __name__ == "__main__": - try: - setup(use_scm_version={"version_scheme": "no-guess-dev"}) - except: # noqa - print( - "\n\nAn error occurred while building the project, " - "please ensure you have the most updated version of setuptools, " - "setuptools_scm and wheel with:\n" - " pip install -U setuptools setuptools_scm wheel\n\n" - ) - raise diff --git a/src/summarizedexperiment/RangedSummarizedExperiment.py b/src/summarizedexperiment/RangedSummarizedExperiment.py index e13f038..20bc195 100644 --- a/src/summarizedexperiment/RangedSummarizedExperiment.py +++ b/src/summarizedexperiment/RangedSummarizedExperiment.py @@ -1,6 +1,7 @@ from __future__ import annotations -from typing import Any, Dict, List, Literal, Optional, Sequence, Union +from collections.abc import Sequence +from typing import Any, Literal, Union from warnings import warn import biocframe @@ -92,13 +93,13 @@ class RangedSummarizedExperiment(SummarizedExperiment): def __init__( self, - assays: Dict[str, Any] = None, - row_ranges: Optional[GRangesOrGRangesList] = None, - row_data: Optional[biocframe.BiocFrame] = None, - column_data: Optional[biocframe.BiocFrame] = None, - row_names: Optional[List[str]] = None, - column_names: Optional[List[str]] = None, - metadata: Optional[Union[Dict[str, Any], ut.NamedList]] = None, + assays: dict[str, Any] = None, + row_ranges: GRangesOrGRangesList | None = None, + row_data: biocframe.BiocFrame | None = None, + column_data: biocframe.BiocFrame | None = None, + row_names: list[str] | None = None, + column_names: list[str] | None = None, + metadata: dict[str, Any] | ut.NamedList | None = None, _validate: bool = True, ) -> None: """Initialize a `RangedSummarizedExperiment` (RSE) object. @@ -274,7 +275,7 @@ def __str__(self) -> str: ) 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"metadata({str(len(self.metadata))}): {ut.print_truncated_list(list(self.metadata.keys()), sep=' ', include_brackets=False, transform=lambda y: y)}" + output += f"metadata({len(self.metadata)!s}): {ut.print_truncated_list(list(self.metadata.keys()), sep=' ', include_brackets=False, transform=lambda y: y)}" return output @@ -291,7 +292,7 @@ def get_row_ranges(self) -> GRangesOrGRangesList: return self._row_ranges def set_row_ranges( - self, row_ranges: Optional[GRangesOrGRangesList], in_place: bool = False + self, row_ranges: GRangesOrGRangesList | None, in_place: bool = False ) -> RangedSummarizedExperiment: """Set new genomic features. @@ -351,7 +352,7 @@ def start(self) -> np.ndarray: return self.row_ranges.start @property - def seqnames(self) -> List[str]: + def seqnames(self) -> list[str]: """Get sequence or chromosome names. Returns: @@ -392,12 +393,9 @@ def seq_info(self) -> SeqInfo: # rest of them are inherited from BaseSE. - def _normalize_row_slice(self, rows: Union[str, int, bool, Sequence]): + def _normalize_row_slice(self, rows: str | int | bool | Sequence): - if isinstance(rows, (GenomicRanges, CompressedGenomicRangesList)): - hits = self.row_ranges.find_overlaps(query=rows) - rows = hits.get_column("self_hits") - elif hasattr(rows, "find_overlaps"): + if isinstance(rows, (GenomicRanges, CompressedGenomicRangesList)) or hasattr(rows, "find_overlaps"): hits = self.row_ranges.find_overlaps(query=rows) rows = hits.get_column("self_hits") @@ -405,8 +403,8 @@ def _normalize_row_slice(self, rows: Union[str, int, bool, Sequence]): def get_slice( self, - rows: Optional[Union[str, int, bool, Sequence]], - columns: Optional[Union[str, int, bool, Sequence]], + rows: str | int | bool | Sequence | None, + columns: str | int | bool | Sequence | None, ) -> RangedSummarizedExperiment: """Alias for :py:attr:`~__getitem__`, for back-compatibility.""" @@ -431,7 +429,7 @@ def get_slice( ######>> range ops <<####### ############################ - def coverage(self, shift: int = 0, width: Optional[int] = None, weight: int = 1) -> Dict[str, np.ndarray]: + def coverage(self, shift: int = 0, width: int | None = None, weight: int = 1) -> dict[str, np.ndarray]: """Calculate coverage for each chromosome. Args: @@ -456,7 +454,7 @@ def nearest( query: GRangesOrRangeSE, select: Literal["all", "arbitrary"] = "all", ignore_strand: bool = False, - ) -> Optional[List[Optional[int]]]: + ) -> list[int | None] | None: """Search nearest positions both upstream and downstream that overlap with each range in ``query``. Args: @@ -496,7 +494,7 @@ def precede( query: GRangesOrRangeSE, select: Literal["all", "arbitrary"] = "all", ignore_strand: bool = False, - ) -> Optional[List[Optional[int]]]: + ) -> list[int | None] | None: """Search nearest positions only downstream that overlap with each range in ``query``. Args: @@ -536,7 +534,7 @@ def follow( query: GRangesOrRangeSE, select: Literal["all", "arbitrary"] = "all", ignore_strand: bool = False, - ) -> Optional[List[Optional[int]]]: + ) -> list[int | None] | None: """Search nearest positions only upstream that overlap with each range in ``query``. Args: @@ -617,7 +615,7 @@ def flank( def resize( self, - width: Union[int, List[int], np.ndarray], + width: int | list[int] | np.ndarray, fix: Literal["start", "end", "center"] = "start", ignore_strand: bool = False, in_place: bool = False, @@ -654,7 +652,7 @@ def resize( output._row_ranges = new_ranges return output - def shift(self, shift: Union[int, List[int], np.ndarray] = 0, in_place: bool = False) -> RangedSummarizedExperiment: + def shift(self, shift: int | list[int] | np.ndarray = 0, in_place: bool = False) -> RangedSummarizedExperiment: """Shift all intervals. ``shift`` may be be negative. @@ -708,8 +706,8 @@ def promoters( 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, in_place: bool = False, ) -> RangedSummarizedExperiment: @@ -742,9 +740,9 @@ def restrict( 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, ) -> RangedSummarizedExperiment: """Narrow genomic positions by provided ``start``, ``width`` and ``end`` parameters. diff --git a/src/summarizedexperiment/SummarizedExperiment.py b/src/summarizedexperiment/SummarizedExperiment.py index c841c55..3a6ac61 100644 --- a/src/summarizedexperiment/SummarizedExperiment.py +++ b/src/summarizedexperiment/SummarizedExperiment.py @@ -1,6 +1,6 @@ from __future__ import annotations -from typing import Any, Dict, List, Optional, Union +from typing import Any from warnings import warn import biocframe @@ -30,12 +30,12 @@ class SummarizedExperiment(BaseSE): def __init__( self, - assays: Dict[str, Any] = None, - row_data: Optional[biocframe.BiocFrame] = None, - column_data: Optional[biocframe.BiocFrame] = None, - row_names: Optional[List[str]] = None, - column_names: Optional[List[str]] = None, - metadata: Optional[Union[Dict[str, Any], ut.NamedList]] = None, + assays: dict[str, Any] = None, + row_data: biocframe.BiocFrame | None = None, + column_data: biocframe.BiocFrame | None = None, + row_names: list[str] | None = None, + column_names: list[str] | None = None, + metadata: dict[str, Any] | ut.NamedList | None = None, _validate: bool = True, ) -> None: """Initialize a Summarized Experiment (SE). diff --git a/src/summarizedexperiment/__init__.py b/src/summarizedexperiment/__init__.py index 728b695..ed383ef 100644 --- a/src/summarizedexperiment/__init__.py +++ b/src/summarizedexperiment/__init__.py @@ -15,5 +15,5 @@ finally: del version, PackageNotFoundError -from .SummarizedExperiment import SummarizedExperiment from .RangedSummarizedExperiment import RangedSummarizedExperiment +from .SummarizedExperiment import SummarizedExperiment diff --git a/src/summarizedexperiment/base.py b/src/summarizedexperiment/base.py index fcb2e34..9f6056f 100644 --- a/src/summarizedexperiment/base.py +++ b/src/summarizedexperiment/base.py @@ -2,7 +2,8 @@ import warnings from collections import OrderedDict, namedtuple -from typing import Any, Dict, List, Optional, Sequence, Tuple, Union +from collections.abc import Sequence +from typing import Any from warnings import warn import biocframe @@ -131,12 +132,12 @@ class BaseSE(ut.BiocObject): def __init__( self, - assays: Dict[str, Any] = None, - row_data: Optional[biocframe.BiocFrame] = None, - column_data: Optional[biocframe.BiocFrame] = None, - row_names: Optional[List[str]] = None, - column_names: Optional[List[str]] = None, - metadata: Optional[Union[Dict[str, Any], ut.NamedList]] = None, + assays: dict[str, Any] = None, + row_data: biocframe.BiocFrame | None = None, + column_data: biocframe.BiocFrame | None = None, + row_names: list[str] | None = None, + column_names: list[str] | None = None, + metadata: dict[str, Any] | ut.NamedList | None = None, _validate: bool = True, ) -> None: """Initialize an instance of ``BaseSE``. @@ -287,7 +288,7 @@ def __len__(self) -> int: return self.shape[0] @property - def shape(self) -> Tuple[int, int]: + def shape(self) -> tuple[int, int]: """Get shape of the experiment. Returns: @@ -298,7 +299,7 @@ def shape(self) -> Tuple[int, int]: return self._shape @property - def dims(self) -> Tuple[int, int]: + def dims(self) -> tuple[int, int]: """Alias to :py:attr:`~summarizedexperiment.BaseSE.BaseSE.shape`. Returns: @@ -356,7 +357,7 @@ def __str__(self) -> str: ) 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"metadata({str(len(self.metadata))}): {ut.print_truncated_list(list(self.metadata.keys()), sep=' ', include_brackets=False, transform=lambda y: y)}" + output += f"metadata({len(self.metadata)!s}): {ut.print_truncated_list(list(self.metadata.keys()), sep=' ', include_brackets=False, transform=lambda y: y)}" return output @@ -364,7 +365,7 @@ def __str__(self) -> str: ######>> assays <<###### ######################## - def get_assays(self) -> Dict[str, Any]: + def get_assays(self) -> dict[str, Any]: """Access assays/experimental data. Returns: @@ -373,7 +374,7 @@ def get_assays(self) -> Dict[str, Any]: """ return self._assays - def set_assays(self, assays: Dict[str, Any], in_place: bool = False) -> BaseSE: + def set_assays(self, assays: dict[str, Any], in_place: bool = False) -> BaseSE: """Set new experiment data (assays). Args: @@ -394,12 +395,12 @@ def set_assays(self, assays: Dict[str, Any], in_place: bool = False) -> BaseSE: return output @property - def assays(self) -> Dict[str, Any]: + def assays(self) -> dict[str, Any]: """Alias for :py:meth:`~get_assays`.""" return self.get_assays() @assays.setter - def assays(self, assays: Dict[str, Any]): + def assays(self, assays: dict[str, Any]): """Alias for :py:meth:`~set_assays` with ``in_place = True``. As this mutates the original object, a warning is raised. @@ -436,7 +437,7 @@ def get_row_data(self, replace_row_names: bool = True) -> biocframe.BiocFrame: def set_row_data( self, - rows: Optional[biocframe.BiocFrame], + rows: biocframe.BiocFrame | None, replace_row_names: bool = False, in_place: bool = False, ) -> BaseSE: @@ -473,12 +474,12 @@ def set_row_data( return output @property - def rowdata(self) -> Dict[str, Any]: + def rowdata(self) -> dict[str, Any]: """Alias for :py:meth:`~get_rowdata`.""" return self.get_row_data() @rowdata.setter - def rowdata(self, rows: Optional[biocframe.BiocFrame]): + def rowdata(self, rows: biocframe.BiocFrame | None): """Alias for :py:meth:`~set_rowdata` with ``in_place = True``. As this mutates the original object, a warning is raised. @@ -490,12 +491,12 @@ def rowdata(self, rows: Optional[biocframe.BiocFrame]): self.set_row_data(rows, in_place=True) @property - def row_data(self) -> Dict[str, Any]: + def row_data(self) -> dict[str, Any]: """Alias for :py:meth:`~get_rowdata`.""" return self.get_row_data() @row_data.setter - def row_data(self, rows: Optional[biocframe.BiocFrame]): + def row_data(self, rows: biocframe.BiocFrame | None): """Alias for :py:meth:`~set_rowdata` with ``in_place = True``. As this mutates the original object, a warning is raised. @@ -532,7 +533,7 @@ def get_column_data(self, replace_row_names: bool = True) -> biocframe.BiocFrame def set_column_data( self, - cols: Optional[biocframe.BiocFrame], + cols: biocframe.BiocFrame | None, replace_column_names: bool = False, in_place: bool = False, ) -> BaseSE: @@ -569,12 +570,12 @@ def set_column_data( return output @property - def columndata(self) -> Dict[str, Any]: + def columndata(self) -> dict[str, Any]: """Alias for :py:meth:`~get_coldata`.""" return self.get_column_data() @columndata.setter - def columndata(self, cols: Optional[biocframe.BiocFrame]): + def columndata(self, cols: biocframe.BiocFrame | None): """Alias for :py:meth:`~set_coldata` with ``in_place = True``. As this mutates the original object, a warning is raised. @@ -586,12 +587,12 @@ def columndata(self, cols: Optional[biocframe.BiocFrame]): self.set_column_data(cols, in_place=True) @property - def coldata(self) -> Dict[str, Any]: + def coldata(self) -> dict[str, Any]: """Alias for :py:meth:`~get_coldata`.""" return self.get_column_data() @coldata.setter - def coldata(self, cols: Optional[biocframe.BiocFrame]): + def coldata(self, cols: biocframe.BiocFrame | None): """Alias for :py:meth:`~set_coldata` with ``in_place = True``. As this mutates the original object, a warning is raised. @@ -603,12 +604,12 @@ def coldata(self, cols: Optional[biocframe.BiocFrame]): self.set_column_data(cols, in_place=True) @property - def column_data(self) -> Dict[str, Any]: + def column_data(self) -> dict[str, Any]: """Alias for :py:meth:`~get_coldata`.""" return self.get_column_data() @column_data.setter - def column_data(self, cols: Optional[biocframe.BiocFrame]): + def column_data(self, cols: biocframe.BiocFrame | None): """Alias for :py:meth:`~set_coldata` with ``in_place = True``. As this mutates the original object, a warning is raised. @@ -620,12 +621,12 @@ def column_data(self, cols: Optional[biocframe.BiocFrame]): self.set_column_data(cols, in_place=True) @property - def col_data(self) -> Dict[str, Any]: + def col_data(self) -> dict[str, Any]: """Alias for :py:meth:`~get_coldata`.""" return self.get_column_data() @col_data.setter - def col_data(self, cols: Optional[biocframe.BiocFrame]): + def col_data(self, cols: biocframe.BiocFrame | None): """Alias for :py:meth:`~set_coldata` with ``in_place = True``. As this mutates the original object, a warning is raised. @@ -640,14 +641,14 @@ def col_data(self, cols: Optional[biocframe.BiocFrame]): ######>> row names <<##### ########################## - def get_row_names(self) -> Optional[ut.Names]: + def get_row_names(self) -> ut.Names | None: """ Returns: List of row names, or None if no row names are available. """ return self._row_names - def set_row_names(self, names: Optional[List[str]], in_place: bool = False) -> BaseSE: + def set_row_names(self, names: list[str] | None, in_place: bool = False) -> BaseSE: """Set new row names. Args: @@ -673,12 +674,12 @@ def set_row_names(self, names: Optional[List[str]], in_place: bool = False) -> B return output @property - def rownames(self) -> Optional[ut.Names]: + def rownames(self) -> ut.Names | None: """Alias for :py:attr:`~get_row_names`, provided for back-compatibility.""" return self.get_row_names() @rownames.setter - def rownames(self, names: Optional[List[str]]): + def rownames(self, names: list[str] | None): """Alias for :py:meth:`~set_row_names` with ``in_place = True``. As this mutates the original object, a warning is raised. @@ -690,12 +691,12 @@ def rownames(self, names: Optional[List[str]]): self.set_row_names(names, in_place=True) @property - def row_names(self) -> Optional[ut.Names]: + def row_names(self) -> ut.Names | None: """Alias for :py:attr:`~get_row_names`, provided for back-compatibility.""" return self.get_row_names() @row_names.setter - def row_names(self, names: Optional[List[str]]): + def row_names(self, names: list[str] | None): """Alias for :py:meth:`~set_row_names` with ``in_place = True``. As this mutates the original object, a warning is raised. @@ -710,14 +711,14 @@ def row_names(self, names: Optional[List[str]]): ######>> column names <<##### ############################# - def get_column_names(self) -> Optional[ut.Names]: + def get_column_names(self) -> ut.Names | None: """ Returns: List of column names, or None if no column names are available. """ return self._column_names - def set_column_names(self, names: Optional[List[str]], in_place: bool = False) -> BaseSE: + def set_column_names(self, names: list[str] | None, in_place: bool = False) -> BaseSE: """Set new column names. Args: @@ -743,12 +744,12 @@ def set_column_names(self, names: Optional[List[str]], in_place: bool = False) - return output @property - def columnnames(self) -> Optional[ut.Names]: + def columnnames(self) -> ut.Names | None: """Alias for :py:attr:`~get_column_names`, provided for back-compatibility.""" return self.get_column_names() @columnnames.setter - def columnnames(self, names: Optional[List[str]]): + def columnnames(self, names: list[str] | None): """Alias for :py:meth:`~set_column_names` with ``in_place = True``. As this mutates the original object, a warning is raised. @@ -760,12 +761,12 @@ def columnnames(self, names: Optional[List[str]]): self.set_column_names(names, in_place=True) @property - def colnames(self) -> Optional[ut.Names]: + def colnames(self) -> ut.Names | None: """Alias for :py:attr:`~get_column_names`, provided for back-compatibility.""" return self.get_column_names() @colnames.setter - def colnames(self, names: Optional[List[str]]): + def colnames(self, names: list[str] | None): """Alias for :py:meth:`~set_column_names` with ``in_place = True``. As this mutates the original object, a warning is raised. @@ -777,12 +778,12 @@ def colnames(self, names: Optional[List[str]]): self.set_column_names(names, in_place=True) @property - def col_names(self) -> Optional[ut.Names]: + def col_names(self) -> ut.Names | None: """Alias for :py:attr:`~get_column_names`, provided for back-compatibility.""" return self.get_column_names() @col_names.setter - def col_names(self, names: Optional[List[str]]): + def col_names(self, names: list[str] | None): """Alias for :py:meth:`~set_column_names` with ``in_place = True``. As this mutates the original object, a warning is raised. @@ -794,12 +795,12 @@ def col_names(self, names: Optional[List[str]]): self.set_column_names(names, in_place=True) @property - def column_names(self) -> Optional[ut.Names]: + def column_names(self) -> ut.Names | None: """Alias for :py:attr:`~get_column_names`, provided for back-compatibility.""" return self.get_column_names() @column_names.setter - def column_names(self, names: Optional[List[str]]): + def column_names(self, names: list[str] | None): """Alias for :py:meth:`~set_column_names` with ``in_place = True``. As this mutates the original object, a warning is raised. @@ -814,7 +815,7 @@ def column_names(self, names: Optional[List[str]]): ######>> assay names <<###### ############################# - def get_assay_names(self) -> List[str]: + def get_assay_names(self) -> list[str]: """Get assay names. Returns: @@ -822,7 +823,7 @@ def get_assay_names(self) -> List[str]: """ return list(self.assays.keys()) - def set_assay_names(self, names: List[str], in_place: bool = False) -> BaseSE: + def set_assay_names(self, names: list[str], in_place: bool = False) -> BaseSE: """Replace :py:attr:`~summarizedexperiment.BaseSE.BaseSE.assays`'s names. Args: @@ -849,12 +850,12 @@ def set_assay_names(self, names: List[str], in_place: bool = False) -> BaseSE: return output @property - def assay_names(self) -> List[str]: + def assay_names(self) -> list[str]: """Alias for :py:attr:`~get_assay_names`.""" return self.get_assay_names() @assay_names.setter - def assay_names(self, names: List[str]): + def assay_names(self, names: list[str]): """Alias for :py:attr:`~set_assay_names` with ``in_place = True``. As this mutates the original object, a warning is raised. @@ -869,7 +870,7 @@ def assay_names(self, names: List[str]): ######>> assay getters <<####### ################################ - def get_assay(self, assay: Union[int, str]) -> Any: + def get_assay(self, assay: int | str) -> Any: """Convenience method to access an :py:attr:`~summarizedexperiment.BaseSE.BaseSE.assays` by name or index. Args: @@ -901,11 +902,11 @@ def get_assay(self, assay: Union[int, str]) -> Any: raise TypeError(f"'assay' must be a string or integer, provided '{type(assay)}'.") - def assay(self, assay: Union[int, str]) -> Any: + def assay(self, assay: int | str) -> Any: """Alias for :py:attr:`~assay`. For backwards compatibility""" return self.get_assay(assay) - def set_assay(self, name: Union[str, int], assay: Any, in_place: bool = False) -> BaseSE: + def set_assay(self, name: str | int, assay: Any, in_place: bool = False) -> BaseSE: """Add or replace :py:attr:`~summarizedexperiment.BaseSE.BaseSE.assays`'s. Args: @@ -958,14 +959,14 @@ def set_assay(self, name: Union[str, int], assay: Any, in_place: bool = False) - ######>> slicers <<####### ########################## - def _normalize_row_slice(self, rows: Union[str, int, bool, Sequence]): + def _normalize_row_slice(self, rows: str | int | bool | Sequence): _scalar = None if not (isinstance(rows, slice) and rows == slice(None)): rows, _scalar = ut.normalize_subscript(rows, len(self._rows), self._row_names) return rows, _scalar - def _normalize_column_slice(self, columns: Union[str, int, bool, Sequence]): + def _normalize_column_slice(self, columns: str | int | bool | Sequence): _scalar = None if not (isinstance(columns, slice) and columns == slice(None)): columns, _scalar = ut.normalize_subscript(columns, len(self._cols), self._column_names) @@ -974,9 +975,9 @@ def _normalize_column_slice(self, columns: Union[str, int, bool, Sequence]): def subset_assays( self, - rows: Optional[Union[str, int, bool, Sequence]], - columns: Optional[Union[str, int, bool, Sequence]], - ) -> Dict[str, Any]: + rows: str | int | bool | Sequence | None, + columns: str | int | bool | Sequence | None, + ) -> dict[str, Any]: """Subset all assays by the slice defined by rows and columns. If both ``row_indices`` and ``col_indices`` are None, a shallow copy of the @@ -1028,8 +1029,8 @@ def subset_assays( def _generic_slice( self, - rows: Optional[Union[str, int, bool, Sequence]], - columns: Optional[Union[str, int, bool, Sequence]], + rows: str | int | bool | Sequence | None, + columns: str | int | bool | Sequence | None, ) -> SliceResult: """Slice ``SummarizedExperiment`` along the rows and/or columns, based on their indices or names. @@ -1085,8 +1086,8 @@ def _generic_slice( def get_slice( self, - rows: Optional[Union[str, int, bool, Sequence]], - columns: Optional[Union[str, int, bool, Sequence]], + rows: str | int | bool | Sequence | None, + columns: str | int | bool | Sequence | None, ) -> BaseSE: """Alias for :py:attr:`~__getitem__`, for back-compatibility.""" @@ -1104,7 +1105,7 @@ def get_slice( def __getitem__( self, - args: Union[int, str, Sequence, tuple], + args: int | str | Sequence | tuple, ) -> BaseSE: """Subset a ``SummarizedExperiment``. diff --git a/src/summarizedexperiment/py.typed b/src/summarizedexperiment/py.typed new file mode 100644 index 0000000..e69de29 diff --git a/tox.ini b/tox.ini index fe005ad..ab663e9 100644 --- a/tox.ini +++ b/tox.ini @@ -1,72 +1,63 @@ -# Tox configuration file -# Read more under https://tox.readthedocs.io/ -# THIS SCRIPT IS SUPPOSED TO BE AN EXAMPLE. MODIFY IT ACCORDING TO YOUR NEEDS! +# Tox configuration file using uv as the backend runner +# Read more under https://tox.wiki/ [tox] -minversion = 3.15 +minversion = 4.0 envlist = default -isolated_build = True - [testenv] description = Invoke pytest to run automated tests -setenv = - TOXINIDIR = {toxinidir} -passenv = - HOME -extras = - testing +extras = testing +deps = twine commands = pytest {posargs} +[testenv:typecheck] +deps = mypy +description = Run static type checking with mypy +commands = + mypy src/ + +[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) -# NOTE: build is still experimental, please refer to the links for updates/issues -# https://setuptools.readthedocs.io/en/stable/build_meta.html#how-to-use-it -# https://github.com/pypa/pep517/issues/91 + build: Build the package + clean: Remove old distribution files +deps = build skip_install = True -changedir = {toxinidir} -deps = - build: build[virtualenv] commands = - clean: python -c 'from shutil import rmtree; rmtree("build", True); rmtree("dist", True)' - build: python -m build . -# By default `build` produces wheels, you can also explicitly use the flags `--sdist` and `--wheel` - + 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} [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 +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 = - TWINE_USERNAME - TWINE_PASSWORD - TWINE_REPOSITORY deps = twine commands = - python -m twine check dist/* - python -m twine upload {posargs:--repository testpypi} dist/* + python -m twine upload {posargs:dist/*}