diff --git a/.github/workflows/publish_pybedlite.yml b/.github/workflows/publish_pybedlite.yml new file mode 100644 index 0000000..f85d1d7 --- /dev/null +++ b/.github/workflows/publish_pybedlite.yml @@ -0,0 +1,129 @@ +name: publish pybedlite + +on: + push: + tags: '\d+.\d+.\d+' + +env: + POETRY_VERSION: 1.6 + +jobs: + on-main-branch-check: + runs-on: ubuntu-latest + outputs: + on_main: ${{ steps.contains_tag.outputs.retval }} + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - uses: rickstaa/action-contains-tag@v1 + id: contains_tag + with: + reference: "main" + tag: "${{ github.ref_name }}" + + tests: + name: tests + needs: on-main-branch-check + if: ${{ needs.on-main-branch-check.outputs.on_main == 'true' }} + uses: "./.github/workflows/tests.yml" + + build-wheels: + name: build wheels + needs: tests + uses: "./.github/workflows/wheels.yml" + + build-sdist: + name: build source distribution + needs: tests + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + submodules: true + + - uses: actions/setup-python@v5 + with: + python-version: 3.12 + + - name: Install poetry + run: | + python -m pip install --upgrade pip + python -m pip install poetry==${{env.POETRY_VERSION}} + + - name: Configure poetry + shell: bash + run: poetry config virtualenvs.in-project true + + - name: Install dependencies + run: poetry install --no-interaction --no-root --without=dev + + - name: Install project + run: poetry install --no-interaction --without=dev + + - name: Build package + run: poetry build --format=sdist + + - uses: actions/upload-artifact@v4 + with: + name: pybedlite-sdist + path: dist/*.tar.gz + + publish-to-pypi: + runs-on: ubuntu-latest + needs: [build-wheels, build-sdist] + environment: pypi + permissions: + id-token: write + steps: + - uses: actions/download-artifact@v4 + with: + path: packages + pattern: 'pybedlite-*' + merge-multiple: true + + - uses: pypa/gh-action-pypi-publish@release/v1 + with: + packages-dir: packages/ + skip-existing: true + verbose: true + + make-changelog: + runs-on: ubuntu-latest + needs: publish-to-pypi + outputs: + release_body: ${{ steps.git-cliff.outputs.content }} + steps: + - name: Checkout the Repository at the Tagged Commit + uses: actions/checkout@v4 + with: + fetch-depth: 0 + ref: ${{ github.ref_name }} + + - name: Generate a Changelog + uses: orhun/git-cliff-action@v3 + id: git-cliff + with: + config: pyproject.toml + args: --latest --verbose + env: + GITHUB_REPO: ${{ github.repository }} + + make-github-release: + runs-on: ubuntu-latest + environment: github + permissions: + contents: write + pull-requests: read + needs: make-changelog + steps: + - name: Create Draft Release + id: create_release + uses: softprops/action-gh-release@v2 + with: + name: ${{ github.ref_name }} + body: ${{ needs.make-changelog.outputs.release_body }} + draft: false + prerelease: false diff --git a/.github/workflows/pythonpackage.yml b/.github/workflows/pythonpackage.yml deleted file mode 100644 index fe3b9cc..0000000 --- a/.github/workflows/pythonpackage.yml +++ /dev/null @@ -1,93 +0,0 @@ -name: Python package - -on: [push] -env: - POETRY_VERSION: 1.6.1 - -jobs: - testing: - runs-on: ubuntu-20.04 - environment: github-action-ci - strategy: - matrix: - PYTHON_VERSION: ["3.8", "3.9", "3.10", "3.11"] - steps: - - uses: actions/checkout@v2 - with: - submodules: 'true' - - - name: Set up Python ${{matrix.PYTHON_VERSION}} - uses: actions/setup-python@v4 - with: - python-version: ${{matrix.PYTHON_VERSION}} - - - name: Get full Python version - id: full-python-version - shell: bash - run: echo ::set-output name=version::$(python -c "import sys; print('-'.join(str(v) for v in sys.version_info))") - - - name: Install poetry - shell: bash - run: | - pip install poetry==${{env.POETRY_VERSION}} - - - name: Configure poetry - shell: bash - run: poetry config virtualenvs.in-project true - - - name: Set up cache - uses: actions/cache@v2 - id: cache - with: - path: .venv - key: venv-${{ runner.os }}-${{ steps.full-python-version.outputs.version }}-${{ hashFiles('**/poetry.lock') }} - - - name: Ensure cache is healthy - if: steps.cache.outputs.cache-hit == 'true' - shell: bash - run: poetry run pip --version >/dev/null 2>&1 || rm -rf .venv - - - name: Install deps - shell: bash - run: | - poetry install --only dev - poetry build - poetry install --extras docs - - - name: Run pytest - shell: bash - run: | - poetry run python -m pytest --cov=pybedlite --cov-report=xml --cov-branch - - - name: Style checking - shell: bash - run: | - poetry run black --line-length 99 --check pybedlite - - - name: Import sorting - shell: bash - run: | - poetry run isort --force-single-line-imports --profile black --check pybedlite - - - name: Run lint - shell: bash - run: | - poetry run flake8 --config=ci/flake8.cfg pybedlite - - - name: Run mypy - shell: bash - run: | - poetry run mypy -p pybedlite --config=ci/mypy.ini - - - name: Run docs - shell: bash - run: | - set -euo pipefail - pushd docs - poetry run make html - popd - - - name: Upload code coverage - uses: codecov/codecov-action@v4.5.0 - with: - token: ${{ secrets.CODECOV_TOKEN }} diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml new file mode 100644 index 0000000..b8a68a5 --- /dev/null +++ b/.github/workflows/tests.yml @@ -0,0 +1,103 @@ +name: tests + +on: + push: + branches: + - "**" + tags: + - "!**" + workflow_call: + +env: + POETRY_VERSION: 1.6 + +jobs: + unit-tests: + runs-on: ubuntu-latest + strategy: + matrix: + PYTHON_VERSION: ["3.8", "3.9", "3.10", "3.11", "3.12"] + + steps: + - uses: actions/checkout@v4 + with: + submodules: "true" + + - name: Set up Python ${{ matrix.PYTHON_VERSION }} + uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.PYTHON_VERSION }} + + - name: Get full Python version + id: full-python-version + shell: bash + run: echo "version=$(python -c "import sys; print('-'.join(str(v) for v in sys.version_info))")" >> $GITHUB_OUTPUT + + - name: Install poetry + run: | + python -m pip install --upgrade pip + python -m pip install poetry==${{env.POETRY_VERSION}} + + - name: Configure poetry + shell: bash + run: poetry config virtualenvs.in-project true + + - name: Set up cache + uses: actions/cache@v4 + id: cache + with: + path: .venv + key: venv-${{ runner.os }}-${{ steps.full-python-version.outputs.version }}-${{ hashFiles('**/poetry.lock') }} + + - name: Ensure cache is healthy + if: steps.cache.outputs.cache-hit == 'true' + shell: bash + run: poetry run pip --version >/dev/null 2>&1 || rm -rf .venv + + - name: Test the lock file is up to date + run: python -m poetry check --lock + + - name: Install dependencies and package + shell: bash + run: | + poetry install --only dev + poetry build + poetry install --extras docs + + - name: Run pytest + shell: bash + run: | + poetry run python -m pytest --cov=pybedlite --cov-report=xml --cov-branch + + - name: Style checking + shell: bash + run: | + poetry run black --line-length 99 --check pybedlite + + - name: Import sorting + shell: bash + run: | + poetry run isort --force-single-line-imports --profile black --check pybedlite + + - name: Run lint + shell: bash + run: | + poetry run flake8 --config=ci/flake8.cfg pybedlite + + - name: Run mypy + shell: bash + run: | + poetry run mypy -p pybedlite --config=ci/mypy.ini + + - name: Run docs + shell: bash + run: | + set -euo pipefail + pushd docs + poetry run make html + popd + + - name: Upload code coverage + uses: codecov/codecov-action@v4.5.0 + with: + token: ${{ secrets.CODECOV_TOKEN }} diff --git a/.github/workflows/wheels.yml b/.github/workflows/wheels.yml index f555d6c..39bf75c 100644 --- a/.github/workflows/wheels.yml +++ b/.github/workflows/wheels.yml @@ -1,17 +1,17 @@ -name: Build wheels for multiple platforms +name: build wheels on: - push: - branches: [main] + pull_request: + workflow_call: workflow_dispatch: jobs: - build_wheels: + build-wheels: name: Build wheels for ${{ matrix.python }}-${{ matrix.platform.target }}_${{ matrix.platform.arch }} on ${{ matrix.platform.os }} runs-on: ${{ matrix.platform.os }} strategy: matrix: - python: [cp38, cp39, cp310, cp311] + python: [cp38, cp39, cp310, cp311, cp312] platform: - os: ubuntu-latest target: manylinux @@ -22,12 +22,12 @@ jobs: # These don't work right now - they just hang while pulling the build image from quay. # If this doesn't resolve itself, we could try to configure different images: # https://cibuildwheel.readthedocs.io/en/stable/options/. - #- os: ubuntu-latest - # target: manylinux - # arch: aarch64 - #- os: ubuntu-latest - # target: musllinux - # arch: aarch64 + # - os: ubuntu-latest + # target: manylinux + # arch: aarch64 + # - os: ubuntu-latest + # target: musllinux + # arch: aarch64 - os: macos-latest target: macosx arch: x86_64 @@ -40,8 +40,7 @@ jobs: with: submodules: "true" - # Used to host cibuildwheel - - uses: actions/setup-python@v4 + - uses: actions/setup-python@v5 with: python-version: 3.8 @@ -55,8 +54,8 @@ jobs: CIBW_BUILD: ${{ matrix.python }}-${{ matrix.platform.target }}_${{ matrix.platform.arch }} CIBW_BUILD_VERBOSITY: 1 - - uses: actions/upload-artifact@v3 + - uses: actions/upload-artifact@v4 with: - name: wheels + name: pybedlite-wheels-${{ matrix.python }}-${{ matrix.platform.target }}_${{ matrix.platform.arch }} path: ./wheelhouse/*.whl if-no-files-found: error diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..e204371 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,69 @@ +# Contributing to pybedlite + +## Getting Setup for Development Work + +Clone the repository to your local machine. +Note that pybedlite includes [cgranges][cgranges-link] as a submodule, so you must use the `--recurse-submodules` option: + +``` +git clone --recurse-submodules https://github.com/fulcrumgenomics/pybedlite.git +``` + +[Poetry][poetry-link] is used to manage the python development environment. + +A simple way to create an environment with the desired version of python and poetry is to use [conda][conda-link]. E.g.: + +```bash +conda create -n pybedlite python=3.8 poetry=1.6 +conda activate pybedlite +poetry install +``` + +If the methods listed above do not work try the following: + +```bash +mamba create -n pybedlite -c conda-forge "python=3.9.16" "poetry=1.6.1" +mamba activate pybedlite +poetry install +``` + +[poetry-link]: https://github.com/python-poetry/poetry +[conda-link]: https://docs.conda.io/en/latest/miniconda.html +[cgranges-link]: https://github.com/lh3/cgranges + +## Checking the Build + +Check the library with: + +```bash +poetry run ./ci/check.sh +``` + +## Building the Documentation + +Build the documentation with: + +```bash +(cd docs/ && poetry run make html) +``` + +## Creating a Release on PyPi + +1. Clone the repository recursively and ensure you are on the `main` (un-dirty) branch +2. Checkout a new branch to prepare the library for release +3. Bump the version of the library to the desired SemVer with `poetry version #.#.#` +4. Commit the version bump changes with a Git commit message like `chore(release): bump to #.#.#` +5. Push the commit to the upstream remote, open a PR, ensure tests pass, and seek reviews +6. Squash merge the PR +7. Tag the new commit on the main branch of the repository with the new SemVer + +GitHub Actions will take care of the remainder of the deploy and release process with: + +1. Unit tests will be run for safety-sake +2. A source distribution will be built +3. Many multi-arch multi-Python binary distributions will be built +4. Assets will be deployed to PyPi with the new SemVer +5. A [Conventional Commit](https://www.conventionalcommits.org/en/v1.0.0/)-aware changelog will be drafted +6. A GitHub release will be created with the new SemVer and the drafted changelog + +Consider editing the changelog if there are any errors or necessary enhancements. diff --git a/README.md b/README.md index cfc230e..b9722ec 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,8 @@ --- -[![Python package][python-package-badge]][python-package-link] +[![Unit tests][tests-badge]][tests-link] +[![Python versions][python-version-badge]](https://github.com/fulcrumgenomics/python-template) [![PyPI version][pypi-badge]][pypi-link] [![PyPI download total][pypi-downloads-badge]][pypi-downloads-link] @@ -26,69 +27,34 @@ [code-coverage-link]: https://codecov.io/gh/fulcrumgenomics/pybedlite [license-badge]: http://img.shields.io/badge/license-MIT-blue.svg [license-link]: https://github.com/fulcrumgenomics/pybedlite/blob/main/LICENSE -[python-package-badge]: https://github.com/fulcrumgenomics/pybedlite/workflows/Python%20package/badge.svg -[python-package-link]: https://github.com/fulcrumgenomics/pybedlite/actions?query=workflow%3A%22Python+package%22 [pypi-badge]: https://badge.fury.io/py/pybedlite.svg [pypi-link]: https://pypi.python.org/pypi/pybedlite [pypi-downloads-badge]: https://img.shields.io/pypi/dm/pybedlite [pypi-downloads-link]: https://pypi.python.org/pypi/pybedlite +[python-version-badge]: https://img.shields.io/badge/python-3.8_|_3.9_|_3.10_|_3.11_|_3.12-blue +[python-version-link]: https://codecov.io/gh/fulcrumgenomics/pybedlite +[tests-badge]: https://github.com/fulcrumgenomics/pybedlite/workflows/tests/badge.svg +[tests-link]: https://github.com/fulcrumgenomics/pybedlite/actions/workflows/tests.yml?query=branch%3Amain # pybedlite See documentation on [pybedlite.readthedocs.org][rtd-link]. -``` +```bash pip install pybedlite ``` OR -``` +```bash conda install -c bioconda pybedlite ``` OR -``` +```bash conda create -n pybedlite pybedlite conda activate pybedlite ``` [rtd-link]: http://pybedlite.readthedocs.org/en/stable -**Requires python 3.8+** (for python < 3.8, please use pybedlite <= 0.0.3) - -# Getting Setup for Development Work - -Clone the repository to your local machine. Note that pybedlite >= 0.0.4 includes [cgranges][cgranges-link] as a submodule, so you must use the `--recurse-submodules` option: -``` -git clone --recurse-submodules https://github.com/fulcrumgenomics/pybedlite.git -``` - -[Poetry][poetry-link] is used to manage the python development environment. - -A simple way to create an environment with the desired version of python and poetry is to use [conda][conda-link]. E.g.: +## Development and Testing -```bash -conda create -n pybedlite python=3.8 poetry -conda activate pybedlite -poetry install -``` - -If the methods listed above do not work try the following: -```bash -mamba create -n pybedlite -c conda-forge "python=3.9.16" "poetry=1.6.1" -mamba activate pybedlite -poetry install -``` - -If, during `poetry install` on Mac OS X errors are encountered running gcc/clang to build `pybedtools` or other packages with native code, try setting the following and re-running `poetry install`: -```bash -export CFLAGS="-stdlib=libc++" -``` - -[poetry-link]: https://github.com/python-poetry/poetry -[conda-link]: https://docs.conda.io/en/latest/miniconda.html -[cgranges-link]: https://github.com/lh3/cgranges - -## Checking the Build -### Run all checks with: -```bash -./ci/check.sh -``` +See the [contributing guide](./CONTRIBUTING.md) for more information. diff --git a/pybedlite/tests/test_overlap_detector.py b/pybedlite/tests/test_overlap_detector.py index 4208fe2..dcaac58 100644 --- a/pybedlite/tests/test_overlap_detector.py +++ b/pybedlite/tests/test_overlap_detector.py @@ -212,8 +212,9 @@ def test_construction_from_ucsc_with_strand(strand: str) -> None: """ `Interval.from_ucsc()` should correctly parse UCSC position-formatted strings with strands. """ + # false positive lint with flake8 below expected_interval = Interval("chr1", 100, 200, negative=(strand == "-")) - assert Interval.from_ucsc(f"chr1:101-200({strand})") == expected_interval + assert Interval.from_ucsc(f"chr1:101-200({strand})") == expected_interval # noqa: E231 @pytest.mark.parametrize( @@ -223,7 +224,8 @@ def test_construction_from_ucsc_other_contigs(contig: str) -> None: """ `Interval.from_ucsc()` should accommodate non-human, decoy, custom, and other contig names. """ - assert Interval.from_ucsc(f"{contig}:101-200") == Interval(contig, 100, 200) + # false positive lint with flake8 below + assert Interval.from_ucsc(f"{contig}:101-200") == Interval(contig, 100, 200) # noqa: E231 def test_that_overlap_detector_allows_generic_parameterization() -> None: diff --git a/pyproject.toml b/pyproject.toml index 06402b2..c45af0f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -24,7 +24,7 @@ classifiers = [ "Topic :: Software Development :: Documentation", "Topic :: Software Development :: Libraries :: Python Modules", ] -include = ["LICENSE"] +include = ["LICENSE", "CONTRIBUTING.md"] packages = [{ include = "pybedlite" }, { include = "cgranges" }] [tool.poetry.dependencies] @@ -50,3 +50,32 @@ generate-setup-file = true [build-system] requires = ["poetry-core", "setuptools", "cython"] build-backend = "poetry.core.masonry.api" + +[tool.git-cliff.changelog] +header = "" +trim = true +body = """ +{% for group, commits in commits | group_by(attribute="group") %} + ## {{ group | upper_first }} + {% for commit in commits %} + - {{ commit.message | upper_first }} ({{ commit.id | truncate(length=8, end="") }})\ + {% endfor %} +{% endfor %}\n +""" + +[tool.git-cliff.git] +conventional_commits = true +commit_parsers = [ + { message = "^.+!:*", group = "Breaking"}, + { message = "^feat*", group = "Features"}, + { message = "^fix*", group = "Bug Fixes"}, + { message = "^docs*", group = "Documentation"}, + { message = "^perf*", group = "Performance"}, + { message = "^refactor*", group = "Refactor"}, + { message = "^style*", group = "Styling"}, + { message = "^test*", group = "Testing"}, + { message = "^chore\\(release\\):*", skip = true}, + { message = "^chore*", group = "Miscellaneous Tasks"}, + { body = ".*security", group = "Security"} +] +filter_commits = false