diff --git a/.coveragerc b/.coveragerc index 30f1dae..7235976 100644 --- a/.coveragerc +++ b/.coveragerc @@ -1,8 +1,8 @@ # .coveragerc to control coverage.py [run] branch = True -source = biocutils -# 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..8ebe519 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/biocutils + 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 e0f247d..25083d6 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 ff68130..1ae9c43 100644 --- a/.gitignore +++ b/.gitignore @@ -53,3 +53,59 @@ docs/html .venv*/ .conda*/ .python-version + +# 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/.readthedocs.yml b/.readthedocs.yml deleted file mode 100644 index a2bcab3..0000000 --- a/.readthedocs.yml +++ /dev/null @@ -1,27 +0,0 @@ -# Read the Docs configuration file -# See https://docs.readthedocs.io/en/stable/config-file/v2.html for details - -# Required -version: 2 - -# Build documentation in the docs/ directory with Sphinx -sphinx: - configuration: docs/conf.py - -# Build documentation with MkDocs -#mkdocs: -# configuration: mkdocs.yml - -# Optionally build your docs in additional formats such as PDF -formats: - - pdf - -build: - os: ubuntu-22.04 - tools: - python: "3.11" - -python: - install: - - requirements: docs/requirements.txt - - {path: ., method: pip} diff --git a/CHANGELOG.md b/CHANGELOG.md index 09f3e72..719fe2c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # Changelog +## Version 0.5.0 + +- Migate the package to `hatch`. + ## Version 0.4.0 - 0.4.2 - Implemented R's `split()` utility to split a sequence by a grouping factor. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index aaa1e60..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 `biocutils` 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 `biocutils`, 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 `biocutils` docs by making them more readable and coherent, or -by adding missing information and correcting mistakes. - -`biocutils` 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 `biocutils`'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 biocutils python=3 six virtualenv pytest pytest-cov -conda activate biocutils -``` - -### 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/biocutils.git - cd biocutils - ``` - -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 - ``` - - `biocutils` 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 -`biocutils`: - -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//biocutils -[issue tracker]: https://github.com//biocutils/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 528312d..1a6314e 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,7 +79,7 @@ # Enable markdown -extensions.append("myst_nb") +extensions.append("myst_parser") # Configure MyST-Parser myst_enable_extensions = [ @@ -118,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 biocutils import __version__ as version -except ImportError: - version = "" + from importlib.metadata import version as get_version + version = get_version("biocutils") +except Exception: + version = "unknown" if not version or version.lower() == "unknown": version = os.getenv("READTHEDOCS_VERSION", "unknown") # automatically set by RTD @@ -167,30 +167,28 @@ # 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__, __init__' -} - -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" + "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/biocutils", + "source_branch": "main", + "source_directory": "docs/", } # Add any paths that contain custom themes here, relative to this directory. @@ -310,7 +308,26 @@ "pandas": ("https://pandas.pydata.org/pandas-docs/stable", None), "scipy": ("https://docs.scipy.org/doc/scipy/reference", None), "setuptools": ("https://setuptools.pypa.io/en/stable/", None), - "pyscaffold": ("https://pyscaffold.org/en/stable", None), } 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 45716dd..307486b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,28 +1,108 @@ +[project] +name = "biocutils" +dynamic = [ + "version", +] +description = "Utilities to use across the biocpy packages." +readme = "README.md" +authors = [ + { name = "Aaron Lun", email = "infinite.monkeys.with.keyboards@gmail.com"}, + { name = "Jayaram Kancherla", email = "jayaram.kancherla@gmail.com" }, +] +requires-python = ">=3.9" +keywords = [ + "bioinformatics", +] +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 = [ + "importlib-metadata>=9.0.0 ; python_full_version < '3.8'", + "numpy", +] + + +[project.license] +file = "LICENSE.txt" + + +[project.urls] +Homepage = "https://github.com/BiocPy/biocutils" +Documentation = "https://biocpy.github.io/biocutils/" +Source = "https://github.com/BiocPy/biocutils" +"Bug Tracker" = "https://github.com/BiocPy/biocutils/issues" + +[project.optional-dependencies] +testing = [ + "pandas>=2.0.0", + "pytest>=7.0.0", + "pytest-cov>=4.0.0", + "scipy", + "setuptools>=68.0.0", +] + [build-system] -# AVOID CHANGING REQUIRES: IT WILL BE UPDATED BY PYSCAFFOLD! -requires = ["setuptools>=46.1.0", "setuptools_scm[toml]>=5"] -build-backend = "setuptools.build_meta" +requires = [ + "hatchling", + "hatch-vcs", +] +build-backend = "hatchling.build" -[tool.setuptools_scm] -# For smarter version schemes and other configuration options, -# check out https://github.com/pypa/setuptools_scm -version_scheme = "no-guess-dev" +[tool.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 289856f..0000000 --- a/setup.cfg +++ /dev/null @@ -1,129 +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 = biocutils -description = Utilities to use across the biocpy packages. -author = Aaron Lun -author_email = infinite.monkeys.with.keyboards@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/biocutils -# Add here related links, for example: -project_urls = - Documentation = https://biocpy.github.io/BiocUtils/ - Source = https://github.com/biocpy/biocutils -# 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" - numpy - - -[options.packages.find] -where = src -exclude = - tests - -[options.extras_require] -# Add here additional requirements for extra features, to install with: -# `pip install biocutils[PDF]` like: -# PDF = ReportLab; RXP - -# Add here test requirements (semicolon/line-separated) -testing = - setuptools - pytest - pytest-cov - pandas - scipy - -[options.entry_points] -# Add here console scripts like: -# console_scripts = -# script_name = biocutils.module:function -# For example: -# console_scripts = -# fibonacci = biocutils.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 biocutils --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 = 88 -extend_ignore = E203, W503 -# ^ Black-compatible -# E203 and W503 have edge cases handled by black -exclude = - .tox - build - dist - .eggs - docs/conf.py - -[pyscaffold] -# PyScaffold's parameters when the project was created. -# This will be used when updating. Do not change! -version = 4.5 -package = biocutils -extensions = - markdown - pre_commit diff --git a/setup.py b/setup.py deleted file mode 100644 index 4fbfd79..0000000 --- a/setup.py +++ /dev/null @@ -1,20 +0,0 @@ -"""Setup file for biocutils. 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/biocutils/BooleanList.py b/src/biocutils/BooleanList.py index 12a32f7..67b0a64 100644 --- a/src/biocutils/BooleanList.py +++ b/src/biocutils/BooleanList.py @@ -1,6 +1,7 @@ from __future__ import annotations -from typing import Any, Iterable, Optional, Sequence, Union +from collections.abc import Iterable, Sequence +from typing import Any from .NamedList import NamedList from .Names import Names @@ -23,7 +24,7 @@ def __init__(self, data: Sequence) -> None: """ self._data = data - def __getitem__(self, index: int) -> Optional[bool]: + def __getitem__(self, index: int) -> bool | None: """Get an item and coerce it to boolean. Args: @@ -46,8 +47,8 @@ class BooleanList(NamedList): def __init__( self, - data: Optional[Sequence] = None, - names: Optional[Names] = None, + data: Sequence | None = None, + names: Names | None = None, _validate: bool = True, ): """ @@ -76,7 +77,7 @@ def __init__( super().__init__(data, names, _validate=_validate) - def set_value(self, index: Union[int, str], value: Any, in_place: bool = False) -> BooleanList: + def set_value(self, index: int | str, value: Any, in_place: bool = False) -> BooleanList: """Calls :py:meth:`~biocutils.NamedList.NamedList.set_value` after coercing ``value`` to a boolean.""" return super().set_value(index, _coerce_to_bool(value), in_place=in_place) @@ -84,7 +85,7 @@ def set_slice(self, index: SubscriptTypes, value: Sequence, in_place: bool = Fal """Calls :py:meth:`~biocutils.NamedList.NamedList.set_slice` after coercing ``value`` to booleans.""" return super().set_slice(index, _SubscriptCoercer(value), in_place=in_place) - def safe_insert(self, index: Union[int, str], value: Any, in_place: bool = False) -> BooleanList: + def safe_insert(self, index: int | str, value: Any, in_place: bool = False) -> BooleanList: """Calls :py:meth:`~biocutils.NamedList.NamedList.safe_insert` after coercing ``value`` to a boolean.""" return super().safe_insert(index, _coerce_to_bool(value), in_place=in_place) diff --git a/src/biocutils/Factor.py b/src/biocutils/Factor.py index dda0175..3b998d3 100644 --- a/src/biocutils/Factor.py +++ b/src/biocutils/Factor.py @@ -1,8 +1,8 @@ from __future__ import annotations import warnings +from collections.abc import Sequence from copy import copy, deepcopy -from typing import Optional, Sequence, Union import numpy @@ -80,7 +80,7 @@ def __iter__(self) -> FactorIterator: """ return self - def __next__(self) -> Union[str, None]: + def __next__(self) -> str | None: """ Returns: Level corresponding to the code at the current position, or None @@ -104,10 +104,10 @@ class Factor: def __init__( self, - codes: Union[numpy.ndarray, Sequence[int]], - levels: Union[StringList, Sequence[str]], + codes: numpy.ndarray | Sequence[int], + levels: StringList | Sequence[str], ordered: bool = False, - names: Optional[Union[Names, Sequence[str]]] = None, + names: Names | Sequence[str] | None = None, _validate: bool = True, ): """Initialize a Factor object. @@ -248,7 +248,7 @@ def names(self) -> Names: """Alias for :py:meth:`~get_names`.""" return self.get_names() - def set_names(self, names: Optional[Names], in_place: bool = False) -> "NamedList": + def set_names(self, names: Names | None, in_place: bool = False) -> NamedList: """ Args: names: @@ -343,7 +343,7 @@ def __eq__(self, other: Factor): #####>>>> Slicing <<<<##### ########################### - def get_value(self, index: Union[str, int]) -> Union[str, None]: + def get_value(self, index: str | int) -> str | None: """ Args: index: @@ -382,7 +382,7 @@ def get_slice(self, index: SubscriptTypes) -> Factor: output._names = subset_sequence(self._names, index) return output - def __getitem__(self, index: SubscriptTypes) -> Union[str, Factor]: + def __getitem__(self, index: SubscriptTypes) -> str | Factor: """ If ``index`` is a scalar, this is an alias for :py:meth:`~get_value`. @@ -394,7 +394,7 @@ def __getitem__(self, index: SubscriptTypes) -> Union[str, Factor]: else: return self.get_slice(NormalizedSubscript(index)) - def set_value(self, index: Union[str, int], value: Union[str, None], in_place: bool = False) -> Factor: + def set_value(self, index: str | int, value: str | None, in_place: bool = False) -> Factor: """ Args: index: @@ -484,7 +484,7 @@ def set_slice(self, index: SubscriptTypes, value: Factor, in_place: bool = False return output - def __setitem__(self, index: SubscriptTypes, value: Union[str, Factor]): + def __setitem__(self, index: SubscriptTypes, value: str | Factor): """ If ``index`` is a scalar, this is an alias for :py:meth:`~set_value`. @@ -590,7 +590,7 @@ def replace_levels( output._levels = new_levels return output - def set_levels(self, levels: Union[str, Sequence[str]], remap: bool = True, in_place: bool = False) -> Factor: + def set_levels(self, levels: str | Sequence[str], remap: bool = True, in_place: bool = False) -> Factor: """ Alias for :py:meth:`~remap_levels` if ``remap = True``, otherwise an alias for :py:meth:`~replace_levels`. The first alias is deprecated and @@ -602,7 +602,7 @@ def set_levels(self, levels: Union[str, Sequence[str]], remap: bool = True, in_p else: return self.replace_levels(levels, in_place=in_place) - def remap_levels(self, levels: Union[str, Sequence[str]], in_place: bool = False) -> Factor: + def remap_levels(self, levels: str | Sequence[str], in_place: bool = False) -> Factor: """Remap codes to a replacement list of levels. Each entry of the remapped ``Factor`` will refer to the same string across the old and new levels, provided that string is present in both sets of levels. @@ -725,10 +725,10 @@ def to_pandas(self): @staticmethod def from_sequence( x: Sequence[str], - levels: Optional[Sequence[str]] = None, + levels: Sequence[str] | None = None, sort_levels: bool = True, ordered: bool = False, - names: Optional[Sequence[str]] = None, + names: Sequence[str] | None = None, **kwargs, ) -> Factor: """Convert a sequence of hashable values into a factor. @@ -778,7 +778,7 @@ def as_list(self) -> list: """ return [self._levels[c] if c >= 0 else None for c in self._codes] - def safe_delete(self, index: Union[int, str, slice], in_place: bool = False) -> Factor: + def safe_delete(self, index: int | str | slice, in_place: bool = False) -> Factor: """ Args: index: @@ -812,11 +812,11 @@ def safe_delete(self, index: Union[int, str, slice], in_place: bool = False) -> return output - def delete(self, index: Union[int, str, slice]): + def delete(self, index: int | str | slice): """Alias for :py:meth:`~safe_delete` with ``in_place = True``.""" self.safe_delete(index, in_place=True) - def __delitem__(self, index: Union[int, str, slice]): + def __delitem__(self, index: int | str | slice): """Alias for :py:meth:`~delete`.""" self.delete(index) diff --git a/src/biocutils/FloatList.py b/src/biocutils/FloatList.py index 5ddd4cc..417c62e 100644 --- a/src/biocutils/FloatList.py +++ b/src/biocutils/FloatList.py @@ -1,6 +1,7 @@ from __future__ import annotations -from typing import Any, Iterable, Optional, Sequence, Union +from collections.abc import Iterable, Sequence +from typing import Any from .NamedList import NamedList from .Names import Names @@ -27,7 +28,7 @@ def __init__(self, data: Sequence) -> None: """ self._data = data - def __getitem__(self, index: int) -> Optional[float]: + def __getitem__(self, index: int) -> float | None: """Get an item and coerce it to float. Args: @@ -49,8 +50,8 @@ class FloatList(NamedList): def __init__( self, - data: Optional[Sequence] = None, - names: Optional[Names] = None, + data: Sequence | None = None, + names: Names | None = None, _validate: bool = True, ): """ @@ -79,7 +80,7 @@ def __init__( super().__init__(data, names, _validate=_validate) - def set_value(self, index: Union[int, str], value: Any, in_place: bool = False) -> FloatList: + def set_value(self, index: int | str, value: Any, in_place: bool = False) -> FloatList: """Calls :py:meth:`~biocutils.NamedList.NamedList.set_value` after coercing ``value`` to a float.""" return super().set_value(index, _coerce_to_float(value), in_place=in_place) @@ -87,7 +88,7 @@ def set_slice(self, index: SubscriptTypes, value: Sequence, in_place: bool = Fal """Calls :py:meth:`~biocutils.NamedList.NamedList.set_slice` after coercing ``value`` to floats.""" return super().set_slice(index, _SubscriptCoercer(value), in_place=in_place) - def safe_insert(self, index: Union[int, str], value: Any, in_place: bool = False) -> FloatList: + def safe_insert(self, index: int | str, value: Any, in_place: bool = False) -> FloatList: """Calls :py:meth:`~biocutils.NamedList.NamedList.safe_insert` after coercing ``value`` to a float.""" return super().safe_insert(index, _coerce_to_float(value), in_place=in_place) diff --git a/src/biocutils/IntegerList.py b/src/biocutils/IntegerList.py index a88e726..68ca0cd 100644 --- a/src/biocutils/IntegerList.py +++ b/src/biocutils/IntegerList.py @@ -1,6 +1,7 @@ from __future__ import annotations -from typing import Any, Iterable, Optional, Sequence, Union +from collections.abc import Iterable, Sequence +from typing import Any from .NamedList import NamedList from .Names import Names @@ -28,7 +29,7 @@ def __init__(self, data: Sequence) -> None: """ self._data = data - def __getitem__(self, index: int) -> Optional[int]: + def __getitem__(self, index: int) -> int | None: """Get an item and coerce it to integer. Args: @@ -51,8 +52,8 @@ class IntegerList(NamedList): def __init__( self, - data: Optional[Sequence] = None, - names: Optional[Names] = None, + data: Sequence | None = None, + names: Names | None = None, _validate: bool = True, ): """ @@ -80,7 +81,7 @@ def __init__( data = list(_coerce_to_int(item) for item in original) super().__init__(data, names, _validate=_validate) - def set_value(self, index: Union[int, str], value: Any, in_place: bool = False) -> IntegerList: + def set_value(self, index: int | str, value: Any, in_place: bool = False) -> IntegerList: """Calls :py:meth:`~biocutils.NamedList.NamedList.set_value` after coercing ``value`` to a integer.""" return super().set_value(index, _coerce_to_int(value), in_place=in_place) @@ -88,7 +89,7 @@ def set_slice(self, index: SubscriptTypes, value: Sequence, in_place: bool = Fal """Calls :py:meth:`~biocutils.NamedList.NamedList.set_slice` after coercing ``value`` to integers.""" return super().set_slice(index, _SubscriptCoercer(value), in_place=in_place) - def safe_insert(self, index: Union[int, str], value: Any, in_place: bool = False) -> IntegerList: + def safe_insert(self, index: int | str, value: Any, in_place: bool = False) -> IntegerList: """Calls :py:meth:`~biocutils.NamedList.NamedList.safe_insert` after coercing ``value`` to a integer.""" return super().safe_insert(index, _coerce_to_int(value), in_place=in_place) diff --git a/src/biocutils/NamedList.py b/src/biocutils/NamedList.py index d421924..fc0cb95 100644 --- a/src/biocutils/NamedList.py +++ b/src/biocutils/NamedList.py @@ -1,7 +1,8 @@ from __future__ import annotations +from collections.abc import Iterable, Sequence from copy import deepcopy -from typing import Any, Dict, Iterable, Optional, Sequence, Tuple, Union +from typing import Any from .assign_sequence import assign_sequence from .combine_sequences import combine_sequences @@ -23,8 +24,8 @@ class NamedList: def __init__( self, - data: Optional[Sequence] = None, - names: Optional[Names] = None, + data: Sequence | None = None, + names: Names | None = None, _validate: bool = True, ): """ @@ -68,7 +69,7 @@ def __len__(self) -> int: """ return len(self._data) - def __iter__(self) -> "list_iterator": + def __iter__(self) -> list_iterator: """ Returns: An iterator on the underlying list of data. @@ -114,7 +115,7 @@ def __eq__(self, other: NamedList) -> bool: #####>>>> Get/set names <<<<##### ################################# - def get_names(self) -> Optional[Names]: + def get_names(self) -> Names | None: """ Returns: Names for the list elements. @@ -125,14 +126,14 @@ def get_names(self) -> Optional[Names]: return self._names @property - def names(self) -> Optional[Names]: + def names(self) -> Names | None: """Alias for :py:meth:`~get_names`.""" return self.get_names() def _shallow_copy(self): return type(self)(self._data, self._names, _validate=False) - def set_names(self, names: Optional[Names], in_place: bool = False) -> NamedList: + def set_names(self, names: Names | None, in_place: bool = False) -> NamedList: """ Args: names: @@ -153,7 +154,7 @@ def set_names(self, names: Optional[Names], in_place: bool = False) -> NamedList output._names = _sanitize_names(names, len(self)) return output - def get_name(self, index: int) -> Optional[str]: + def get_name(self, index: int) -> str | None: """Get name at an index. Args: @@ -171,7 +172,7 @@ def get_name(self, index: int) -> Optional[str]: #####>>>> Get/set items <<<<##### ################################# - def get_value(self, index: Union[str, int]) -> Any: + def get_value(self, index: str | int) -> Any: """Get value at an index. Args: @@ -207,7 +208,7 @@ def get_slice(self, index: SubscriptTypes) -> NamedList: outnames = subset_sequence(self._names, index) return type(self)(outdata, outnames, _validate=False) - def __getitem__(self, index: SubscriptTypes) -> Union[NamedList, Any]: + def __getitem__(self, index: SubscriptTypes) -> NamedList | Any: """ If ``index`` is a scalar, this is an alias for :py:meth:`~get_value`. @@ -219,7 +220,7 @@ def __getitem__(self, index: SubscriptTypes) -> Union[NamedList, Any]: else: return self.get_slice(NormalizedSubscript(index)) - def set_value(self, index: Union[str, int], value: Any, in_place: bool = False) -> NamedList: + def set_value(self, index: str | int, value: Any, in_place: bool = False) -> NamedList: """ Args: index: @@ -335,7 +336,7 @@ def _define_output(self, in_place: bool) -> NamedList: else: return self.copy() - def safe_insert(self, index: Union[int, str], value: Any, in_place: bool = False) -> NamedList: + def safe_insert(self, index: int | str, value: Any, in_place: bool = False) -> NamedList: """ Args: index: @@ -363,7 +364,7 @@ def safe_insert(self, index: Union[int, str], value: Any, in_place: bool = False output._names.insert(index, "") return output - def insert(self, index: Union[int, str], value: Any): + def insert(self, index: int | str, value: Any): """Alias for :py:meth:`~safe_insert` with ``in_place = True``.""" self.safe_insert(index, value, in_place=True) @@ -435,7 +436,7 @@ def __iadd__(self, other: list): self.extend(other) return self - def safe_delete(self, index: Union[int, str, slice], in_place: bool = False) -> NamedList: + def safe_delete(self, index: int | str | slice, in_place: bool = False) -> NamedList: """ Args: index: @@ -468,11 +469,11 @@ def safe_delete(self, index: Union[int, str, slice], in_place: bool = False) -> return output - def delete(self, index: Union[int, str, slice]): + def delete(self, index: int | str | slice): """Alias for :py:meth:`~safe_delete` with ``in_place = True``.""" self.safe_delete(index, in_place=True) - def __delitem__(self, index: Union[int, str, slice]): + def __delitem__(self, index: int | str | slice): """Alias for :py:meth:`~delete`.""" self.delete(index) @@ -496,7 +497,7 @@ def values(self) -> Iterable[Any]: """ return iter(self._data) - def items(self) -> Iterable[Tuple[str, Any]]: + def items(self) -> Iterable[tuple[str, Any]]: """ Returns: Iterator over (name, value) pairs. @@ -507,7 +508,7 @@ def items(self) -> Iterable[Tuple[str, Any]]: else: return zip((str(i) for i in range(len(self))), self._data) - def get(self, key: Union[str, int], default: Any = None) -> Any: + def get(self, key: str | int, default: Any = None) -> Any: """ Args: key: @@ -576,7 +577,7 @@ def as_list(self) -> list: """ return self._data - def as_dict(self) -> Dict[str, Any]: + def as_dict(self) -> dict[str, Any]: """ Returns: A dictionary where the keys are the names and the values are the @@ -611,7 +612,7 @@ def from_dict(cls, x: dict) -> NamedList: A instance where the list elements are the values of ``x`` and the names are the stringified keys. """ - return cls(list(x.values()), names=Names(str(y) for y in x.keys())) + return cls(list(x.values()), names=Names(str(y) for y in x)) @subset_sequence.register diff --git a/src/biocutils/Names.py b/src/biocutils/Names.py index 1047f79..f869175 100644 --- a/src/biocutils/Names.py +++ b/src/biocutils/Names.py @@ -1,7 +1,8 @@ from __future__ import annotations +from collections.abc import Callable, Iterable, Sequence from copy import deepcopy -from typing import Any, Callable, Iterable, List, Optional, Sequence, Union +from typing import Any, Union from .assign_sequence import assign_sequence from .combine_sequences import combine_sequences @@ -18,7 +19,7 @@ class Names: such that callers can get or set elements by name instead of position. """ - def __init__(self, names: Optional[Iterable] = None, _validate: bool = True): + def __init__(self, names: Iterable | None = None, _validate: bool = True): """ Args: names: @@ -63,7 +64,7 @@ def __len__(self) -> int: """ return len(self._names) - def __iter__(self) -> "list_iterator": + def __iter__(self) -> list_iterator: """ Returns: An iterator on the underlying list of names. @@ -96,7 +97,7 @@ def __eq__(self, other: Names) -> bool: return False return self._names == other._names - def as_list(self) -> List[str]: + def as_list(self) -> list[str]: """ Returns: List of strings containing the names. @@ -160,7 +161,7 @@ def get_slice(self, index: SubscriptTypes) -> Names: index, scalar = normalize_subscript(index, len(self), None) return type(self)(subset_sequence(self._names, index), _validate=False) - def __getitem__(self, index: SubscriptTypes) -> Union[str, Names]: + def __getitem__(self, index: SubscriptTypes) -> str | Names: """ If ``index`` is a scalar, this is an alias for :py:attr:`~get_value`. @@ -339,7 +340,7 @@ def __iadd__(self, other: list): self.extend(other) return self - def safe_delete(self, index: Union[int, slice], in_place: bool = False) -> Names: + def safe_delete(self, index: int | slice, in_place: bool = False) -> Names: """ Args: index: @@ -360,11 +361,11 @@ def safe_delete(self, index: Union[int, slice], in_place: bool = False) -> Names del output._names[index] return output - def delete(self, index: Union[int, slice]): + def delete(self, index: int | slice): """Alias for :py:attr:`~safe_delete` with ``in_place = True``.""" self.safe_delete(index, in_place=True) - def __delitem__(self, index: Union[int, slice]): + def __delitem__(self, index: int | slice): """Alias for :py:attr:`~delete`.""" self.delete(index) @@ -427,7 +428,7 @@ def _combine_sequences_Names(*x: Names) -> Names: return output -def _name_to_position(names: Optional[Names], index: str) -> int: +def _name_to_position(names: Names | None, index: str) -> int: i = -1 if names is not None: i = names.map(index) @@ -436,14 +437,14 @@ def _name_to_position(names: Optional[Names], index: str) -> int: return i -def _validate_names(names: Optional[Names], length: int) -> bool: +def _validate_names(names: Names | None, length: int) -> bool: if names is not None and len(names) != length: raise ValueError("length of 'names' must be equal to number of entries (" + str(length) + ")") return True -def _sanitize_names(names: Optional[Names], length: int) -> Optional[Names]: +def _sanitize_names(names: Names | None, length: int) -> Names | None: if names is None: return names if not isinstance(names, Names): @@ -453,7 +454,7 @@ def _sanitize_names(names: Optional[Names], length: int) -> Optional[Names]: return names -def _combine_names(*x: Any, get_names: Callable) -> Optional[Names]: +def _combine_names(*x: Any, get_names: Callable) -> Names | None: all_names = [] has_names = False for y in x: diff --git a/src/biocutils/StringList.py b/src/biocutils/StringList.py index dee7f17..95a34e3 100644 --- a/src/biocutils/StringList.py +++ b/src/biocutils/StringList.py @@ -1,6 +1,7 @@ from __future__ import annotations -from typing import Any, Iterable, Optional, Sequence, Union +from collections.abc import Iterable, Sequence +from typing import Any from .NamedList import NamedList from .Names import Names @@ -22,7 +23,7 @@ def __init__(self, data: Sequence) -> None: """ self._data = data - def __getitem__(self, index: int) -> Optional[str]: + def __getitem__(self, index: int) -> str | None: """Get an item and coerce it to string. Args: @@ -44,8 +45,8 @@ class StringList(NamedList): def __init__( self, - data: Optional[Sequence] = None, - names: Optional[Names] = None, + data: Sequence | None = None, + names: Names | None = None, _validate: bool = True, ): """ @@ -73,7 +74,7 @@ def __init__( data = list(_coerce_to_str(item) for item in original) super().__init__(data, names, _validate=_validate) - def set_value(self, index: Union[int, str], value: Any, in_place: bool = False) -> StringList: + def set_value(self, index: int | str, value: Any, in_place: bool = False) -> StringList: """Calls :py:meth:`~biocutils.NamedList.NamedList.set_value` after coercing ``value`` to a string.""" return super().set_value(index, _coerce_to_str(value), in_place=in_place) @@ -81,7 +82,7 @@ def set_slice(self, index: SubscriptTypes, value: Sequence, in_place: bool = Fal """Calls :py:meth:`~biocutils.NamedList.NamedList.set_slice` after coercing ``value`` to strings.""" return super().set_slice(index, _SubscriptCoercer(value), in_place=in_place) - def safe_insert(self, index: Union[int, str], value: Any, in_place: bool = False) -> StringList: + def safe_insert(self, index: int | str, value: Any, in_place: bool = False) -> StringList: """Calls :py:meth:`~biocutils.NamedList.NamedList.safe_insert` after coercing ``value`` to a string.""" return super().safe_insert(index, _coerce_to_str(value), in_place=in_place) diff --git a/src/biocutils/__init__.py b/src/biocutils/__init__.py index 7900178..d03693e 100644 --- a/src/biocutils/__init__.py +++ b/src/biocutils/__init__.py @@ -15,56 +15,45 @@ finally: del version, PackageNotFoundError -from .Factor import Factor -from .StringList import StringList -from .IntegerList import IntegerList -from .FloatList import FloatList +from .assign import assign +from .assign_rows import assign_rows +from .assign_sequence import assign_sequence +from .biocobject import BiocObject from .BooleanList import BooleanList -from .Names import Names -from .NamedList import NamedList - +from .combine import combine +from .combine_columns import combine_columns +from .combine_rows import combine_rows +from .combine_sequences import combine_sequences +from .convert_to_dense import convert_to_dense +from .duplicated import duplicated, unique +from .extract_column_names import extract_column_names +from .extract_row_names import extract_row_names +from .Factor import Factor from .factorize import factorize +from .FloatList import FloatList +from .get_height import get_height +from .IntegerList import IntegerList from .intersect import intersect +from .is_high_dimensional import is_high_dimensional from .is_list_of_type import is_list_of_type from .is_missing_scalar import is_missing_scalar from .map_to_index import map_to_index -from .match import match, create_match_index, MatchIndex -from .normalize_subscript import normalize_subscript, SubscriptTypes +from .match import MatchIndex, create_match_index, match +from .NamedList import NamedList +from .Names import Names +from .normalize_subscript import SubscriptTypes, normalize_subscript +from .order import order, sort from .print_truncated import print_truncated, print_truncated_dict, print_truncated_list from .print_wrapped_table import create_floating_names, print_type, print_wrapped_table, truncate_strings -from .setdiff import setdiff -from .union import union - -from .combine import combine -from .combine_rows import combine_rows -from .combine_columns import combine_columns -from .combine_sequences import combine_sequences - from .relaxed_combine_columns import relaxed_combine_columns from .relaxed_combine_rows import relaxed_combine_rows - -from .extract_row_names import extract_row_names -from .extract_column_names import extract_column_names - +from .setdiff import setdiff +from .show_as_cell import show_as_cell +from .split import split +from .StringList import StringList from .subset import subset from .subset_rows import subset_rows from .subset_sequence import subset_sequence - -from .which import which - -from .assign import assign -from .assign_rows import assign_rows -from .assign_sequence import assign_sequence - -from .show_as_cell import show_as_cell -from .convert_to_dense import convert_to_dense - -from .get_height import get_height -from .is_high_dimensional import is_high_dimensional - -from .biocobject import BiocObject from .table import table - -from .order import order, sort -from .duplicated import duplicated, unique -from .split import split +from .union import union +from .which import which diff --git a/src/biocutils/assign.py b/src/biocutils/assign.py index 875033d..abe8add 100644 --- a/src/biocutils/assign.py +++ b/src/biocutils/assign.py @@ -1,4 +1,5 @@ -from typing import Any, Sequence +from collections.abc import Sequence +from typing import Any from .assign_rows import assign_rows from .assign_sequence import assign_sequence diff --git a/src/biocutils/assign_rows.py b/src/biocutils/assign_rows.py index ada797b..7c4e146 100644 --- a/src/biocutils/assign_rows.py +++ b/src/biocutils/assign_rows.py @@ -1,6 +1,7 @@ +from collections.abc import Sequence from copy import deepcopy from functools import singledispatch -from typing import Any, Sequence +from typing import Any import numpy diff --git a/src/biocutils/assign_sequence.py b/src/biocutils/assign_sequence.py index d6b23d8..cd73d24 100644 --- a/src/biocutils/assign_sequence.py +++ b/src/biocutils/assign_sequence.py @@ -1,6 +1,7 @@ +from collections.abc import Sequence from copy import deepcopy from functools import singledispatch -from typing import Any, Sequence, Union +from typing import Any import numpy @@ -48,7 +49,7 @@ def _assign_sequence_numpy(x: numpy.ndarray, indices: Sequence[int], replacement @assign_sequence.register -def _assign_sequence_range(x: range, indices: Sequence[int], replacement: Any) -> Union[range, list]: +def _assign_sequence_range(x: range, indices: Sequence[int], replacement: Any) -> range | list: if ( isinstance(replacement, range) and isinstance(indices, range) diff --git a/src/biocutils/biocobject.py b/src/biocutils/biocobject.py index 422b7a6..5d17fad 100644 --- a/src/biocutils/biocobject.py +++ b/src/biocutils/biocobject.py @@ -1,7 +1,7 @@ from __future__ import annotations import copy -from typing import Any, Dict, Optional, Union +from typing import Any from warnings import warn from .NamedList import NamedList @@ -40,7 +40,7 @@ class BiocObject: Provides a standardized `metadata` slot and copy-on-write semantics. """ - def __init__(self, metadata: Optional[Union[Dict[str, Any], NamedList]] = None, _validate: bool = True) -> None: + def __init__(self, metadata: dict[str, Any] | NamedList | None = None, _validate: bool = True) -> None: """Initialize the BiocObject. Args: @@ -82,7 +82,7 @@ def metadata(self) -> NamedList: return self._metadata @metadata.setter - def metadata(self, metadata: Optional[Union[Dict[str, Any], NamedList]]) -> None: + def metadata(self, metadata: dict[str, Any] | NamedList | None) -> None: """Set metadata in-place.""" warn( "Setting property 'metadata' is an in-place operation, use 'set_metadata' instead", @@ -94,7 +94,7 @@ def get_metadata(self) -> NamedList: """Alias for :py:attr:`~metadata` getter.""" return self.metadata - def set_metadata(self, metadata: Optional[Union[Dict[str, Any], NamedList]], in_place: bool = False) -> BiocObject: + def set_metadata(self, metadata: dict[str, Any] | NamedList | None, in_place: bool = False) -> BiocObject: """Set new metadata. Args: diff --git a/src/biocutils/duplicated.py b/src/biocutils/duplicated.py index 303af2a..8202cea 100644 --- a/src/biocutils/duplicated.py +++ b/src/biocutils/duplicated.py @@ -1,5 +1,6 @@ +from collections.abc import Sequence from functools import singledispatch -from typing import Any, Sequence, Union +from typing import Any import numpy @@ -8,7 +9,7 @@ @singledispatch -def duplicated(x: Any, incomparables: Union[set, Sequence] = set(), from_last: bool = False) -> numpy.ndarray: +def duplicated(x: Any, incomparables: set | Sequence = set(), from_last: bool = False) -> numpy.ndarray: """ Find duplicated elements of ``x``. @@ -101,9 +102,7 @@ def process(i, y): @duplicated.register -def _duplicated_Factor( - x: Factor, incomparables: Union[set, Sequence] = set(), from_last: bool = False -) -> numpy.ndarray: +def _duplicated_Factor(x: Factor, incomparables: set | Sequence = set(), from_last: bool = False) -> numpy.ndarray: present = [] for lev in x.get_levels(): if lev in incomparables: @@ -140,7 +139,7 @@ def process(i, y): return output -def unique(x: Any, incomparables: Union[set, Sequence] = set(), from_last: bool = False) -> Any: +def unique(x: Any, incomparables: set | Sequence = set(), from_last: bool = False) -> Any: """ Get all unique values of ``x``. diff --git a/src/biocutils/factorize.py b/src/biocutils/factorize.py index 904d24d..b1ef2dd 100644 --- a/src/biocutils/factorize.py +++ b/src/biocutils/factorize.py @@ -1,4 +1,4 @@ -from typing import Optional, Sequence, Tuple +from collections.abc import Sequence import numpy @@ -8,11 +8,11 @@ def factorize( x: Sequence, - levels: Optional[Sequence] = None, + levels: Sequence | None = None, sort_levels: bool = False, - dtype: Optional[numpy.dtype] = None, - fail_missing: Optional[bool] = None, -) -> Tuple[list, numpy.ndarray]: + dtype: numpy.dtype | None = None, + fail_missing: bool | None = None, +) -> tuple[list, numpy.ndarray]: """Convert a sequence of hashable values into a factor. Args: diff --git a/src/biocutils/intersect.py b/src/biocutils/intersect.py index f441492..7b0f5cf 100644 --- a/src/biocutils/intersect.py +++ b/src/biocutils/intersect.py @@ -1,4 +1,4 @@ -from typing import Sequence +from collections.abc import Sequence from .is_missing_scalar import is_missing_scalar from .map_to_index import DUPLICATE_METHOD diff --git a/src/biocutils/is_list_of_type.py b/src/biocutils/is_list_of_type.py index 5f0ff54..1410fe0 100644 --- a/src/biocutils/is_list_of_type.py +++ b/src/biocutils/is_list_of_type.py @@ -1,14 +1,14 @@ -from typing import Callable, Union +from collections.abc import Callable import numpy as np -import numpy.ma as ma +from numpy import ma __author__ = "jkanche" __copyright__ = "jkanche" __license__ = "MIT" -def is_list_of_type(x: Union[list, tuple], target_type: Callable, ignore_none: bool = False) -> bool: +def is_list_of_type(x: list | tuple, target_type: Callable, ignore_none: bool = False) -> bool: """Checks if ``x`` is a list, and whether all elements of the list are of the same type. Args: diff --git a/src/biocutils/map_to_index.py b/src/biocutils/map_to_index.py index fc50b6d..22eca0f 100644 --- a/src/biocutils/map_to_index.py +++ b/src/biocutils/map_to_index.py @@ -1,4 +1,5 @@ -from typing import Literal, Sequence +from collections.abc import Sequence +from typing import Literal from .is_missing_scalar import is_missing_scalar diff --git a/src/biocutils/match.py b/src/biocutils/match.py index 1ebaaf1..b9a60cc 100644 --- a/src/biocutils/match.py +++ b/src/biocutils/match.py @@ -1,5 +1,6 @@ +from collections.abc import Sequence from functools import singledispatch -from typing import Any, Literal, Optional, Sequence, Union +from typing import Any, Literal import numpy @@ -14,9 +15,9 @@ def __init__( self, targets: Any, duplicate_method: Literal["first", "last", "any"] = "first", - incomparables: Union[set, Sequence] = set(), - dtype: Optional[numpy.dtype] = None, - fail_missing: Optional[bool] = None, + incomparables: set | Sequence = set(), + dtype: numpy.dtype | None = None, + fail_missing: bool | None = None, ): """ Args: @@ -141,9 +142,9 @@ def match(self, x: Any) -> numpy.ndarray: def create_match_index( targets: Any, duplicate_method: Literal["first", "last", "any"] = "first", - incomparables: Union[set, Sequence] = set(), - dtype: Optional[numpy.dtype] = None, - fail_missing: Optional[bool] = None, + incomparables: set | Sequence = set(), + dtype: numpy.dtype | None = None, + fail_missing: bool | None = None, ) -> MatchIndex: """ Create a index for matching an arbitrary sequence against ``targets``. @@ -229,9 +230,9 @@ def match( x: Any, targets: Any, duplicate_method: Literal["first", "last", "any"] = "first", - incomparables: Union[set, Sequence] = set(), - dtype: Optional[numpy.dtype] = None, - fail_missing: Optional[bool] = None, + incomparables: set | Sequence = set(), + dtype: numpy.dtype | None = None, + fail_missing: bool | None = None, ) -> numpy.ndarray: """ Find a matching value of each element of ``x`` in ``targets``. diff --git a/src/biocutils/normalize_subscript.py b/src/biocutils/normalize_subscript.py index e41bb92..f9ad762 100644 --- a/src/biocutils/normalize_subscript.py +++ b/src/biocutils/normalize_subscript.py @@ -1,4 +1,5 @@ -from typing import Any, Optional, Sequence, Tuple, Union +from collections.abc import Sequence +from typing import Any, Union import numpy @@ -63,9 +64,9 @@ def __len__(self) -> int: def normalize_subscript( sub: SubscriptTypes, length: int, - names: Optional[Sequence[str]] = None, + names: Sequence[str] | None = None, non_negative_only: bool = True, -) -> Tuple[Sequence[int], bool]: +) -> tuple[Sequence[int], bool]: """Normalize a subscript into a sequence of integer indices. Normalize a subscript for ``__getitem__`` or friends into a sequence of diff --git a/src/biocutils/order.py b/src/biocutils/order.py index dacefca..17f9581 100644 --- a/src/biocutils/order.py +++ b/src/biocutils/order.py @@ -1,18 +1,19 @@ -from typing import Any, Union, Sequence, Optional +from collections.abc import Sequence from functools import singledispatch +from typing import Any import numpy -from .subset import subset from .Factor import Factor +from .subset import subset @singledispatch def order( x: Any, - force_last: Union[set, Sequence] = [None, numpy.ma.masked, numpy.nan], + force_last: set | Sequence = [None, numpy.ma.masked, numpy.nan], decreasing: bool = False, - dtype: Optional[numpy.dtype] = None, + dtype: numpy.dtype | None = None, ) -> numpy.ndarray: """ Obtain an ordering of entries of ``x``. @@ -150,9 +151,9 @@ def key(i): @order.register def _order_Factor( x: Factor, - force_last: Union[set, Sequence] = set([None]), + force_last: set | Sequence = set([None]), decreasing: bool = False, - dtype: Optional[numpy.dtype] = None, + dtype: numpy.dtype | None = None, ) -> numpy.ndarray: new_force_last = set() for i, lev in enumerate(x.get_levels()): @@ -166,7 +167,7 @@ def _order_Factor( @singledispatch -def sort(x: Any, force_last: Union[set, Sequence] = [None, numpy.ma.masked], decreasing: bool = False) -> Any: +def sort(x: Any, force_last: set | Sequence = [None, numpy.ma.masked], decreasing: bool = False) -> Any: """ Sort an arbitrary iterable sequence. diff --git a/src/biocutils/package_utils.py b/src/biocutils/package_utils.py index 55238e5..15d130b 100644 --- a/src/biocutils/package_utils.py +++ b/src/biocutils/package_utils.py @@ -3,6 +3,9 @@ __license__ = "MIT" +import importlib.util + + def is_package_installed(package_name: str, verbose: bool = False) -> bool: """Check if a package is installed. @@ -13,14 +16,12 @@ def is_package_installed(package_name: str, verbose: bool = False) -> bool: Returns: True if package is installed, otherwise False. """ - _installed = False try: - exec(f"import {package_name}") - _installed = True + _installed = importlib.util.find_spec(package_name) is not None except Exception: - if verbose: - print(f"Package '{package_name}' is not installed.") + _installed = False - pass + if not _installed and verbose: + print(f"Package '{package_name}' is not installed.") return _installed diff --git a/src/biocutils/print_truncated.py b/src/biocutils/print_truncated.py index 11b529a..d2b1018 100644 --- a/src/biocutils/print_truncated.py +++ b/src/biocutils/print_truncated.py @@ -1,4 +1,4 @@ -from typing import Callable, Dict, List, Optional +from collections.abc import Callable def print_truncated(x, truncated_to: int = 3, full_threshold: int = 10) -> str: @@ -28,10 +28,10 @@ def print_truncated(x, truncated_to: int = 3, full_threshold: int = 10) -> str: def print_truncated_list( - x: List, + x: list, truncated_to: int = 3, full_threshold: int = 10, - transform: Optional[Callable] = None, + transform: Callable | None = None, sep: str = ", ", include_brackets: bool = True, ) -> str: @@ -87,10 +87,10 @@ def transform(y): def print_truncated_dict( - x: Dict, + x: dict, truncated_to: int = 3, full_threshold: int = 10, - transform: Optional[Callable] = None, + transform: Callable | None = None, sep: str = ", ", include_brackets: bool = True, ) -> str: diff --git a/src/biocutils/print_wrapped_table.py b/src/biocutils/print_wrapped_table.py index 447c8e7..20173f6 100644 --- a/src/biocutils/print_wrapped_table.py +++ b/src/biocutils/print_wrapped_table.py @@ -1,23 +1,23 @@ -from typing import Any, List, Optional, Sequence +from collections.abc import Sequence +from typing import Any import numpy from .subset_sequence import subset_sequence -def _get_max_width(col: List[str]): +def _get_max_width(col: list[str]): width = 0 for y in col: - if len(y) > width: - width = len(y) + width = max(width, len(y)) return width def print_wrapped_table( - columns: List[Sequence[str]], - floating_names: Optional[Sequence[str]] = None, + columns: list[Sequence[str]], + floating_names: Sequence[str] | None = None, sep: str = " ", - window: Optional[int] = None, + window: int | None = None, ) -> str: """Pretty-print a table with aligned and wrapped columns. All column contents are padded so that they are right- justified. Wrapping is performed whenever a new column would exceed the window width, in which case the entire @@ -102,7 +102,7 @@ def reinitialize(): return output -def create_floating_names(names: Optional[List[str]], indices: Sequence[int]) -> List[str]: +def create_floating_names(names: list[str] | None, indices: Sequence[int]) -> list[str]: """Create the floating names to use in :py:meth:`~print_wrapped_table`. If no names are present, positional indices are used instead. @@ -122,7 +122,7 @@ def create_floating_names(names: Optional[List[str]], indices: Sequence[int]) -> return ["[" + str(i) + "]" for i in indices] -def truncate_strings(values: List[str], width: int = 40) -> List[str]: +def truncate_strings(values: list[str], width: int = 40) -> list[str]: """Truncate long strings for printing in :py:meth:`~print_wrapped_table`. Args: diff --git a/src/biocutils/py.typed b/src/biocutils/py.typed new file mode 100644 index 0000000..e69de29 diff --git a/src/biocutils/reverse_index.py b/src/biocutils/reverse_index.py index 4d1f2e0..e930711 100644 --- a/src/biocutils/reverse_index.py +++ b/src/biocutils/reverse_index.py @@ -1,4 +1,4 @@ -from typing import Sequence +from collections.abc import Sequence def build_reverse_index(obj: Sequence[str]) -> dict: diff --git a/src/biocutils/setdiff.py b/src/biocutils/setdiff.py index 1511c4b..13b0bfe 100644 --- a/src/biocutils/setdiff.py +++ b/src/biocutils/setdiff.py @@ -1,12 +1,10 @@ from collections.abc import Sequence +from functools import singledispatch from .is_missing_scalar import is_missing_scalar from .map_to_index import DUPLICATE_METHOD -from functools import singledispatch - - @singledispatch def _setdiff_internal(first: Sequence, *other: Sequence, duplicate_method: DUPLICATE_METHOD = "first") -> list: present = set() diff --git a/src/biocutils/show_as_cell.py b/src/biocutils/show_as_cell.py index 072c17f..5c4f3fb 100644 --- a/src/biocutils/show_as_cell.py +++ b/src/biocutils/show_as_cell.py @@ -1,9 +1,10 @@ +from collections.abc import Sequence from functools import singledispatch -from typing import Any, List, Sequence +from typing import Any @singledispatch -def show_as_cell(x: Any, indices: Sequence[int]) -> List[str]: +def show_as_cell(x: Any, indices: Sequence[int]) -> list[str]: """ Show the contents of ``x`` as a cell of a table, typically for use in the ``__str__`` method of a class that contains ``x``. diff --git a/src/biocutils/split.py b/src/biocutils/split.py index 20bb769..ccb24b5 100644 --- a/src/biocutils/split.py +++ b/src/biocutils/split.py @@ -1,5 +1,6 @@ +from collections.abc import Sequence from functools import singledispatch -from typing import Any, Sequence, Union +from typing import Any import numpy @@ -14,10 +15,10 @@ def split( x: Any, f: Sequence, - skip: Union[set, Sequence] = [None, numpy.ma.masked], + skip: set | Sequence = [None, numpy.ma.masked], drop: bool = False, as_NamedList: bool = False, -) -> Union[dict, NamedList]: +) -> dict | NamedList: """ Split a sequence ``x`` into groups defined by a categorical factor ``f``. diff --git a/src/biocutils/subset.py b/src/biocutils/subset.py index 7ef1260..3363a4c 100644 --- a/src/biocutils/subset.py +++ b/src/biocutils/subset.py @@ -1,4 +1,5 @@ -from typing import Any, Sequence +from collections.abc import Sequence +from typing import Any from .is_high_dimensional import is_high_dimensional from .subset_rows import subset_rows diff --git a/src/biocutils/subset_rows.py b/src/biocutils/subset_rows.py index e5b31cb..0fde0ef 100644 --- a/src/biocutils/subset_rows.py +++ b/src/biocutils/subset_rows.py @@ -1,5 +1,6 @@ +from collections.abc import Sequence from functools import singledispatch -from typing import Any, Sequence +from typing import Any import numpy diff --git a/src/biocutils/subset_sequence.py b/src/biocutils/subset_sequence.py index 1c39517..d7e0c3e 100644 --- a/src/biocutils/subset_sequence.py +++ b/src/biocutils/subset_sequence.py @@ -1,5 +1,6 @@ +from collections.abc import Sequence from functools import singledispatch -from typing import Any, Sequence, Union +from typing import Any @singledispatch @@ -36,7 +37,7 @@ def _subset_sequence_list(x: list, indices: Sequence[int]) -> list: @subset_sequence.register -def _subset_sequence_range(x: range, indices: Sequence[int]) -> Union[list, range]: +def _subset_sequence_range(x: range, indices: Sequence[int]) -> list | range: """Subset a range by indices. Args: diff --git a/src/biocutils/table.py b/src/biocutils/table.py index 72d06f5..8b175da 100644 --- a/src/biocutils/table.py +++ b/src/biocutils/table.py @@ -1,5 +1,5 @@ +from collections.abc import Sequence from functools import singledispatch -from typing import Sequence from .IntegerList import IntegerList diff --git a/src/biocutils/union.py b/src/biocutils/union.py index 78e3949..1ca5628 100644 --- a/src/biocutils/union.py +++ b/src/biocutils/union.py @@ -1,4 +1,4 @@ -from typing import Sequence +from collections.abc import Sequence from .is_missing_scalar import is_missing_scalar from .map_to_index import DUPLICATE_METHOD diff --git a/src/biocutils/which.py b/src/biocutils/which.py index ca7a863..9008e72 100644 --- a/src/biocutils/which.py +++ b/src/biocutils/which.py @@ -1,11 +1,11 @@ -from typing import Optional, Sequence +from collections.abc import Sequence import numpy def which( x: Sequence, - dtype: Optional[numpy.dtype] = None, + dtype: numpy.dtype | None = None, ) -> numpy.ndarray: """Report the indices of all elements of ``x`` that are truthy. diff --git a/tox.ini b/tox.ini index 69f8159..828f4db 100644 --- a/tox.ini +++ b/tox.ini @@ -1,93 +1,62 @@ # Tox configuration file # Read more under https://tox.wiki/ -# THIS SCRIPT IS SUPPOSED TO BE AN EXAMPLE. MODIFY IT ACCORDING TO YOUR NEEDS! [tox] -minversion = 3.24 +minversion = 4.0 envlist = default -isolated_build = True - [testenv] description = Invoke pytest to run automated tests -setenv = - TOXINIDIR = {toxinidir} -passenv = - HOME - SETUPTOOLS_* -extras = - testing +extras = testing commands = pytest {posargs} +[testenv:typecheck] +description = Run static type checking with mypy +deps = mypy +commands = + mypy src/ -# # To run `tox -e lint` you need to make sure you have a -# # `.pre-commit-config.yaml` file. See https://pre-commit.com -# [testenv:lint] -# description = Perform static analysis and style checks -# skip_install = True -# deps = pre-commit -# passenv = -# HOMEPATH -# PROGRAMDATA -# SETUPTOOLS_* -# commands = -# pre-commit run --all-files {posargs:--show-diff-on-failure} - +[testenv:lint] +description = Perform static analysis and style checks +deps = ruff +skip_install = True +commands = + ruff check {posargs:.} + ruff format --check {posargs:.} [testenv:{build,clean}] description = - build: Build the package in isolation according to PEP517, see https://github.com/pypa/build - clean: Remove old distribution files and temporary build artifacts (./build and ./dist) -# https://setuptools.pypa.io/en/stable/build_meta.html#how-to-use-it + build: Build the package + clean: Remove old distribution files +deps = build skip_install = True -changedir = {toxinidir} -deps = - build: build[virtualenv] -passenv = - SETUPTOOLS_* commands = clean: python -c 'import shutil; [shutil.rmtree(p, True) for p in ("build", "dist", "docs/_build")]' clean: python -c 'import pathlib, shutil; [shutil.rmtree(p, True) for p in pathlib.Path("src").glob("*.egg-info")]' build: python -m build {posargs} -# By default, both `sdist` and `wheel` are built. If your sdist is too big or you don't want -# to make it available, consider running: `tox -e build -- --wheel` - [testenv:{docs,doctests,linkcheck}] description = docs: Invoke sphinx-build to build the docs doctests: Invoke sphinx-build to run doctests linkcheck: Check for broken links in the documentation -passenv = - SETUPTOOLS_* +deps = + -r {toxinidir}/docs/requirements.txt setenv = DOCSDIR = {toxinidir}/docs BUILDDIR = {toxinidir}/docs/_build docs: BUILD = html doctests: BUILD = doctest linkcheck: BUILD = linkcheck -deps = - -r {toxinidir}/docs/requirements.txt - # ^ requirements.txt shared with Read The Docs commands = + sphinx-apidoc -f -o "{env:DOCSDIR}/api" src/ sphinx-build --color -b {env:BUILD} -d "{env:BUILDDIR}/doctrees" "{env:DOCSDIR}" "{env:BUILDDIR}/{env:BUILD}" {posargs} - [testenv:publish] description = Publish the package you have been developing to a package index server. - By default, it uses testpypi. If you really want to publish your package - to be publicly accessible in PyPI, use the `-- --repository pypi` option. skip_install = True -changedir = {toxinidir} -passenv = - # See: https://twine.readthedocs.io/en/latest/ - TWINE_USERNAME - TWINE_PASSWORD - TWINE_REPOSITORY - TWINE_REPOSITORY_URL deps = twine commands = - python -m twine check dist/* - python -m twine upload {posargs:--repository {env:TWINE_REPOSITORY:testpypi}} dist/* + python -m twine upload {posargs:dist/*}