diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index ba51306..030dfa1 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -15,8 +15,8 @@ env: PYTHON_VERSION: 3.11 jobs: - packages-build: - name: Build packages + checks: + name: Check code runs-on: ubuntu-latest env: RUFF_FORMAT: github @@ -29,30 +29,70 @@ jobs: with: python-version: ${{ env.PYTHON_VERSION }} - - name: Prepare virtual environment - run: make create-env + - uses: astral-sh/setup-uv@v4 - name: Check format run: | make check-format || true BOLDRED=$(tput bold && tput setaf 1) RESET=$(tput sgr0) - echo "${BOLDRED}==> We won't fail on formatting errors for the time being, but we will in the future.${RESET}" + echo "${BOLDRED}==> We won't penalise formatting errors for the time being, but we will in the future.${RESET}" - name: Check lint run: | make check-lint || true BOLDRED=$(tput bold && tput setaf 1) RESET=$(tput sgr0) - echo "${BOLDRED}==> We won't fail on lint errors for the time being, but we will in the future.${RESET}" + echo "${BOLDRED}==> We won't enforce linting errors for the time being, but we will in the future.${RESET}" - - name: Build packages - run: make build-dist + sdist-build: + name: Build sdist + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: ${{ env.PYTHON_VERSION }} + + - uses: astral-sh/setup-uv@v4 + + - name: Build sdist + run: make build-sdist + + - name: Upload packages + uses: actions/upload-artifact@v4 + with: + name: sdist + path: dist + + wheels-build: + name: Build wheels + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest, macos-latest] + + steps: + - uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: ${{ env.PYTHON_VERSION }} + + - uses: astral-sh/setup-uv@v4 + + - name: Build wheels + run: make build-wheels - name: Upload packages uses: actions/upload-artifact@v4 with: - name: python-packages + name: wheels-${{ matrix.os }} path: dist docker-build: @@ -67,19 +107,24 @@ jobs: with: python-version: ${{ env.PYTHON_VERSION }} - - name: Prepare virtual environment - run: make create-env + - uses: astral-sh/setup-uv@v4 - name: Check Dockerfile run: make check-docker - name: Build Docker image - run: make build-image + run: make save-image # TODO: Enable this when we figure out how to run it without having to download several Gigabytes of data. # - name: Test Docker image # run: make run-example + - name: Upload Docker image + uses: actions/upload-artifact@v4 + with: + name: docker-image + path: oncodrivefml.tar + docs-build: name: Build documentation runs-on: ubuntu-latest @@ -92,10 +137,9 @@ jobs: with: python-version: ${{ env.PYTHON_VERSION }} - - name: Prepare virtual environment - run: make create-env + - uses: astral-sh/setup-uv@v4 - - name: Check version matching the tag + - name: Build the docs shell: bash run: make docs @@ -110,7 +154,9 @@ jobs: runs-on: ubuntu-latest if: startsWith(github.ref, 'refs/tags/') needs: - - packages-build + - checks + - sdist-build + - wheels-build - docker-build - docs-build @@ -122,8 +168,7 @@ jobs: with: python-version: ${{ env.PYTHON_VERSION }} - - name: Prepare virtual environment - run: make create-env + - uses: astral-sh/setup-uv@v4 - name: Check version matching the tag run: make check-version @@ -134,6 +179,8 @@ jobs: if: startsWith(github.ref, 'refs/tags/') needs: - check-version + - sdist-build + - wheels-build steps: - uses: actions/checkout@v4 @@ -143,13 +190,12 @@ jobs: with: python-version: ${{ env.PYTHON_VERSION }} - - name: Prepare virtual environment - run: make create-env + - uses: astral-sh/setup-uv@v4 - name: Download packages uses: actions/download-artifact@v4 with: - name: python-packages + path: dist - name: Publish to PyPI env: @@ -164,16 +210,19 @@ jobs: - check-version steps: - - if: ${{ env.DOCKER_USERNAME != '' }} - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Set up Python uses: actions/setup-python@v5 with: python-version: ${{ env.PYTHON_VERSION }} - - name: Prepare virtual environment - run: make create-env + - uses: astral-sh/setup-uv@v4 + + - name: Download Docker image + uses: actions/download-artifact@v4 + with: + name: docker-image - name: Login to DockerHub env: @@ -182,4 +231,4 @@ jobs: run: make docker-login - name: Push Docker image - run: make push-image + run: make load-image push-image diff --git a/.gitignore b/.gitignore index 1983ed2..aab8789 100644 --- a/.gitignore +++ b/.gitignore @@ -38,6 +38,9 @@ docs/build # External users tests external_checks +# VSCode +.vscode/ + .ruff_cache/ .venv data/ diff --git a/CHANGELOG.rst b/CHANGELOG.md similarity index 80% rename from CHANGELOG.rst rename to CHANGELOG.md index 4ee44cb..a7587d8 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.md @@ -1,9 +1,11 @@ -Changelog -========= +# Changelog -2.4.0 ------ +## 2.5.0 + +- Adopted uv as a build tool and added a CI/CD pipeline + +## 2.4.0 - The ``--no-indels`` flags only discards indels, it cannot enable their analysis @@ -13,19 +15,14 @@ Changelog - Added seed as option in the configuration file - Updated dependencies and dropped Python 3.5 support -2.3.0 ------ +## 2.3.0 - Added option to set the size limit for the indels -2.2.0 ------ +## 2.2.0 - Simplified signature computation using bgsignature. Complex options should be computed externally. - - Adapted to bgparsers 0.9 that require a regions file with header. - - Simplified indels computation as assuming everything in the positive strand. - -- Set by default the indels computation as substitutions. \ No newline at end of file +- Set by default the indels computation as substitutions. diff --git a/Makefile b/Makefile index ce0934f..cdab556 100644 --- a/Makefile +++ b/Makefile @@ -6,18 +6,24 @@ DOCS_DIR := $(ROOT_DIR)/docs VENV_DIR := $(ROOT_DIR)/.venv define version -$(shell $(VENV_DIR)/bin/python -c 'from oncodrivefml import __version__; print(__version__)') +$(shell uv run python -c "from oncodrivefml import __version__; print(__version__)") +endef + +define git_tag_or_sha +$(shell git describe --tags --exact-match 2>/dev/null || git rev-parse --short HEAD) endef define image -bbglab/oncodrivefml:$(version) +bbglab/oncodrivefml:$(call version) endef -GIT_TAG_OR_SHA = $(shell git describe --tags --exact-match 2>/dev/null || git rev-parse --short HEAD) +IMAGE_FILE := oncodrivefml.tar BOLDRED := $(shell tput bold && tput setaf 1) BOLDGREEN := $(shell tput bold && tput setaf 2) BOLDYELLOW := $(shell tput bold && tput setaf 3) +BOLDBLUE := $(shell tput bold && tput setaf 4) +LIGHTBLUE := $(shell tput setaf 6) WHITE := $(shell tput sgr0 && tput setaf 7) RESET := $(shell tput sgr0) @@ -32,9 +38,6 @@ help: @echo "$(BOLDGREEN) check-docker $(WHITE)-> Check the Dockerfile" @echo "$(BOLDGREEN) format $(WHITE)-> Format source code" @echo "$(BOLDGREEN) build-dist $(WHITE)-> Build source and wheel distribution files" - @echo "$(BOLDGREEN) install-dev $(WHITE)-> Install the packages in editable mode" - @echo "$(BOLDGREEN) create-env $(WHITE)-> Create a virtual environment" - @echo "$(BOLDGREEN) remove-env $(WHITE)-> Remove the virtual environment" @echo "$(BOLDGREEN) build-image $(WHITE)-> Build the Docker image" @echo "$(BOLDGREEN) docker-login $(WHITE)-> Log in to DockerHub" @echo "$(BOLDGREEN) push-image $(WHITE)-> Push the Docker image into DockerHub" @@ -43,25 +46,47 @@ help: @echo "$(BOLDGREEN) clean $(WHITE)-> Clean the working directory (build files, virtual environments, caches)" @echo "$(RESET)" -$(VENV_DIR): - @echo "$(BOLDYELLOW)Preparing virtual environment ...$(RESET)" - python -m venv $(VENV_DIR) - $(VENV_DIR)/bin/pip install -U pip ruff setuptools wheel build twine - @echo "$(BOLDGREEN)==> Success!$(RESET)" +.PHONY: uv-installed +uv-installed: + @if ! which uv > /dev/null; then \ + echo "$(BOLDRED)This project build is managed by $(BOLDYELLOW)uv$(BOLDRED), which is not installed.$(RESET)"; \ + echo "$(LIGHTBLUE)Please follow these instructions to install it:$(RESET)"; \ + echo "$(LIGHTBLUE)--> $(BOLDBLUE)https://docs.astral.sh/uv/#getting-started$(RESET)"; \ + exit 1; \ + fi + +.PHONY: ruff-installed +ruff-installed: uv-installed + @if ! which ruff > /dev/null; then \ + echo "$(BOLDRED)This project requires $(BOLDYELLOW)ruff$(BOLDRED), which is not installed.$(RESET)"; \ + echo "$(LIGHTBLUE)Installing it with $(BOLDYELLOW)uv tool install ruff$(RESET)"; \ + uv tool install ruff; \ + ruff --version; \ + fi + +.PHONY: sphinx-installed +sphinx-installed: uv-installed + @if ! uv pip show sphinx > /dev/null; then \ + echo "$(BOLDRED)This project requires $(BOLDYELLOW)sphinx$(BOLDRED), which is not installed.$(RESET)"; \ + echo "$(LIGHTBLUE)Installing it with $(BOLDYELLOW)uv pip install --requirements optional-requirements.txt$(RESET)"; \ + uv venv; \ + uv pip install --requirements optional-requirements.txt; \ + uv run sphinx-build --version; \ + fi .PHONY: checks checks: check-format check-lint check-docker .PHONY: check-format -check-format: $(VENV_DIR) +check-format: ruff-installed @echo "$(BOLDGREEN)Checking code format ...$(RESET)" - $(VENV_DIR)/bin/ruff format --check + ruff format --check @echo "$(BOLDGREEN)==> Success!$(RESET)" .PHONY: check-lint -check-lint: $(VENV_DIR) +check-lint: ruff-installed @echo "$(BOLDGREEN)Checking lint ...$(RESET)" - $(VENV_DIR)/bin/ruff check + ruff check @echo "$(BOLDGREEN)==> Success!$(RESET)" .PHONY: check-docker @@ -75,51 +100,43 @@ check-docker: @echo "$(BOLDGREEN)==> Success!$(RESET)" .PHONY: check-version -check-version: $(VENV_DIR) +check-version: uv-installed @echo "$(BOLDGREEN)Checking that the version matches the tag ...$(RESET)" - @if [ "$(version)" != "$(GIT_TAG_OR_SHA)" ]; then \ - echo "$(BOLDRED)==> Version $(BOLDYELLOW)$(version)$(BOLDRED) doesn't match the git tag $(BOLDYELLOW)$(GIT_TAG_OR_SHA)$(BOLDRED) !!!$(RESET)"; \ + @if [ "$(call version)" != "$(call git_tag_or_sha)" ]; then \ + echo "$(BOLDRED)==> Version $(BOLDYELLOW)$(call version)$(BOLDRED) doesn't match the git tag $(BOLDYELLOW)$(call git_tag_or_sha)$(BOLDRED) !!!$(RESET)"; \ echo "$(BOLDRED)==> Please update the $(BOLDYELLOW)__version__$(BOLDRED) in $(BOLDYELLOW)oncodrivefml/__init__.py$(BOLDRED) and re-create the tag.$(RESET)"; \ exit 1; \ fi @echo "$(BOLDGREEN)==> Success!$(RESET)" .PHONY: format -format: $(VENV_DIR) +format: ruff-installed @echo "$(BOLDGREEN)Formatting code ...$(RESET)" - $(VENV_DIR)/bin/ruff format + ruff format .PHONY: build-dist -build-dist: $(VENV_DIR) +build-dist: uv-installed @echo "$(BOLDGREEN)Building packages ...$(RESET)" - $(VENV_DIR)/bin/python -m build + uv build + +.PHONY: build-sdist +build-sdist: + uv build --sdist + +.PHONY: build-wheels +build-wheels: uv-installed + uv venv + uv pip install cibuildwheel + uv run cibuildwheel --output-dir dist .PHONY: publish-dist -publish-dist: $(VENV_DIR) - @echo "$(BOLDGREEN)Publishing OncodriveFML $(BOLDYELLOW)$(version)$(BOLDGREEN) to PyPI ...$(RESET)" - @if [[ -z "$(PYPI_TOKEN)" ]]; then \ - echo "$(BOLDRED)==> Missing PYPI_TOKEN !!!$(RESET)"; \ +publish-dist: uv-installed + @echo "$(BOLDGREEN)Publishing OncodriveCLUSTL $(BOLDYELLOW)$(call version)$(BOLDGREEN) to PyPI ...$(RESET)" + @if [ -z "$(PYPI_TOKEN)" ]; then \ + echo "$(BOLDRED)==> Missing PyPI token !!!$(RESET)"; \ exit 1; \ fi - @$(VENV_DIR)/bin/twine upload --username __token__ --password $(PYPI_TOKEN) dist/* - -.PHONY: install -install-dev: $(VENV_DIR) - $(VENV_DIR)/bin/pip install -e . - -.PHONY: create-env -create-env: $(VENV_DIR) - -.PHONY: remove-env -remove-env: - @echo "$(BOLDGREEN)Removing virtual environment ...$(RESET)" - rm -rf $(VENV_DIR) - -.PHONY: build-image -build-image: $(VENV_DIR) - @echo "$(BOLDGREEN)Building Docker image $(BOLDYELLOW)$(image)$(BOLDGREEN) ...$(RESET)" - docker build --progress=plain -t $(image) . - @echo "$(BOLDGREEN)==> Success!$(RESET)" + uv publish --token $(PYPI_TOKEN) .PHONY: docker-login docker-login: @@ -131,20 +148,35 @@ docker-login: @(echo "$(DOCKER_PASSWORD)" | docker login -u $(DOCKER_USERNAME) --password-stdin) || (echo "$(BOLDRED)==> Failed to log in !!!$(RESET)"; exit 1) .PHONY: build-image -push-image: $(VENV_DIR) +build-image: uv-installed + @echo "$(BOLDGREEN)Building Docker image $(BOLDYELLOW)$(call image)$(BOLDGREEN) ...$(RESET)" + docker build --progress=plain -t $(call image) . + @echo "$(BOLDGREEN)==> Success!$(RESET)" + +.PHONY: save-image +save-image: build-image + @echo "$(BOLDGREEN)Saving Docker image $(BOLDYELLOW)$(call image)$(BOLDGREEN) ...$(RESET)" + docker save -o $(IMAGE_FILE) $(call image) + @echo "$(BOLDGREEN)==> Success!$(RESET)" + +.PHONY: load-image +load-image: uv-installed + @echo "$(BOLDGREEN)Loading Docker image $(BOLDYELLOW)$(call image)$(BOLDGREEN) ...$(RESET)" + docker load -i $(IMAGE_FILE) + @echo "$(BOLDGREEN)==> Success!$(RESET)" + +.PHONY: build-image +push-image: uv-installed @echo "$(BOLDGREEN)Pushing the Docker image into the DockerHub ...$(RESET)" - docker push $(image) + docker push $(call image) @echo "$(BOLDGREEN)==> Success!$(RESET)" .PHONY: docs -docs: $(VENV_DIR) - @if ! which $(VENV_DIR)/bin/sphinx-build > /dev/null; then \ - $(VENV_DIR)/bin/pip install -r optional-requirements.txt; \ - fi - (source $(VENV_DIR)/bin/activate; make -C $(DOCS_DIR) html) +docs: sphinx-installed + (source .venv/bin/activate; make -C $(DOCS_DIR) html) .PHONY: run-example -run-example: +run-example: build-image @echo "$(BOLDGREEN)Running example ...$(RESET)" docker run --rm -i \ -v $${BGDATA_LOCAL:-$${HOME}/.bgdata}:/root/.bgdata \ @@ -156,6 +188,6 @@ run-example: .PHONY: clean clean: @echo "$(BOLDGREEN)Cleaning the repository ...$(RESET)" - rm -rf ./oncodrivefml.egg-info ./dist $(VENV_DIR) ./.ruff_cache ./.eggs $(DOCS_DIR)/build + rm -rf ./oncodrivefml.egg-info ./dist ./.ruff_cache ./.eggs $(DOCS_DIR)/build ./.venv find oncodrivefml \( -name '*.c' -o -name '*.so' \) -type f -exec rm {} + find . -name "__pycache__" -type d -exec rm -r {} + diff --git a/oncodrivefml/__init__.py b/oncodrivefml/__init__.py index c5090b3..6511cee 100644 --- a/oncodrivefml/__init__.py +++ b/oncodrivefml/__init__.py @@ -1,2 +1,2 @@ __logger_name__ = 'oncodrivefml' -__version__ = "2.5.0" \ No newline at end of file +__version__ = "2.5.0rc1" \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..fea8ff3 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,89 @@ +[project] +name = "oncodrivefml" +dynamic = ["version"] +description = "Identify signals of positive selection in somatic mutations" +readme = "README.md" +license = {file = "LICENSE"} +authors = [ + { name = "BBGLab (Barcelona Biomedical Genomics Lab)", email = "bbglab@irbbarcelona.org" }, +] +requires-python = ">=3.8.0,<3.12.0" +dependencies = [ + "ago==0.0.93", + "bgcache==0.1", + "bgconfig==0.10", + "bgdata>=2.0.4", + "bglogs==0.6", + "bgparsers==0.10", + "bgreference==0.6", + "bgsignature==0.2", + "bokeh==1.4.0", + "click==8.0.4", + "intervaltree==3.1.0", + "Jinja2==3.0.3", + "numpy==1.24.4", + "matplotlib==3.6.3", + "pandas==1.5.3", + "pytabix==0.1", + "scipy==1.10.1", + "statsmodels==0.14.0", +] + +[project.scripts] +oncodrivefml = "oncodrivefml.main:cmdline" + +[project.urls] +Download = "https://pypi.org/project/oncodrivefml/#history" +Homepage = "https://github.com/bbglab/oncodrivefml" + +[build-system] +requires = ["hatchling", "Cython==0.29.37"] +build-backend = "hatchling.build" + +[tool.hatch.version] +path = "oncodrivefml/__init__.py" + +[tool.hatch.build.targets.sdist] +include = ["/oncodrivefml"] +exclude = ["*.c", "*.so"] + +[tool.hatch.build.targets.wheel] +macos-max-compat = false +packages = ["extra", "oncodrivefml"] +exclude = ["*.c", "*.so"] +include = [ + "oncodrivefml/*.txt.gz", + "oncodrivefml/*.conf.template", + "oncodrivefml/*.conf.template.spec", + "oncodrivefml/*.pyx", + "oncodrivefml/*.json.gz" +] + +[tool.hatch.build.targets.wheel.hooks.cython] +dependencies = ["hatch-cython"] + +[tool.hatch.build.targets.wheel.hooks.cython.options] +compile_py = false + +[tool.setuptools] +packages = ["extra", "oncodrivefml"] + +[tool.cibuildwheel] +build-frontend = "build[uv]" +skip = "pp* *-manylinux_i686 *musllinux*" +enable = [] + +# [build-system] +# requires = ["setuptools>=74.1", "Cython==0.29.37"] +# build-backend = "setuptools.build_meta" + +# [tool.setuptools] +# ext-modules = [ +# { name = "oncodrivefml.walker_cython", sources = ["oncodrivefml/walker_cython.pyx"] } +# ] + +# [tool.setuptools.packages] +# find = {} + +# [tool.setuptools.package-data] +# oncodrivefml = ["*.txt.gz", "*.conf.template", "*.conf.template.spec", "*.pyx", "*.json.gz"] diff --git a/setup.py b/setup.py deleted file mode 100644 index cc2a20f..0000000 --- a/setup.py +++ /dev/null @@ -1,34 +0,0 @@ - -from os import path - -from setuptools import Extension -from setuptools import setup, find_packages -from oncodrivefml import __version__ - - -directory = path.dirname(path.abspath(__file__)) -with open(path.join(directory, 'requirements.txt')) as f: - required = f.read().splitlines() - -setup( - name='oncodrivefml', - version=__version__, - packages=find_packages(), - package_data={'oncodrivefml': ['*.txt.gz', '*.conf.template', '*.conf.template.spec', '*.pyx', '*.json.gz']}, - url="https://bitbucket.org/bbglab/oncodrivefml", - download_url="https://bitbucket.org/bbglab/oncodrivefml/get/"+__version__+".tar.gz", - license='UPF Free Source Code', - author='BBGLab (Barcelona Biomedical Genomics Lab)', - author_email='bbglab@irbbarcelona.org', - description='Identify signals of positive selection in somatic mutations', - setup_requires=[ - 'cython', - ], - install_requires=required, - ext_modules=[Extension('oncodrivefml.walker_cython', ['oncodrivefml/walker_cython.pyx'])], - entry_points={ - 'console_scripts': [ - 'oncodrivefml = oncodrivefml.main:cmdline' - ] - } -)