Skip to content

Use uv for CI build and deploy on GitHub action and CircleCI #618

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 20 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 17 additions & 10 deletions .circleci/config.yml
Original file line number Diff line number Diff line change
@@ -1,16 +1,19 @@
# Python CircleCI 2.1 configuration file
#
# As much as possible, this file should be kept in sync with:
# https://github.com/napari/napari/blob/main/.circleci/config.yaml
# Use the latest 2.1 version of CircleCI pipeline process engine.
# See: https://circleci.com/docs/2.1/configuration-reference
#
# Check for more details: https://circleci.com/docs/2.1/configuration-reference
version: 2.1

# Orbs are reusable packages of CircleCI configuration that you may share across projects.
# See: https://circleci.com/docs/2.1/orb-intro/
orbs:
python: circleci/python@3.0.0

jobs:
build-docs:
docker:
# A list of available CircleCI Docker convenience images are available here: https://circleci.com/developer/images/image/cimg/python
- image: cimg/python:3.10.16
steps:
- checkout:
Expand All @@ -21,24 +24,28 @@ jobs:
- run:
name: Install qt libs + xvfb
command: sudo apt-get update && sudo apt-get install -y xvfb libegl1 libdbus-1-3 libxkbcommon-x11-0 libxcb-icccm4 libxcb-image0 libxcb-keysyms1 libxcb-randr0 libxcb-render-util0 libxcb-xinerama0 libxcb-xinput0 libxcb-xfixes0 x11-utils
# [TODO] Remove this step once uv is installed by default in the cimg/python:3.10 image
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does uv is available on more modern images (3.11 or 3.12)?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The PR adding uv was merged into the image 3 days ago (thanks @psobolewskiPhD). Unfortunately, it has been over a month since the images have been updated on docker hub. There are a few other issues that may prevent quick release https://github.com/CircleCI-Public/cimg-python/issues so I think this is a reasonable approach until the release happens in the future.

- run:
name: install uv
command: |
curl -LsSf https://astral.sh/uv/install.sh | sh
uv -V
- run:
name: Setup virtual environment
command: |
python -m venv venv
. venv/bin/activate
python -m pip install --upgrade pip

uv venv
. .venv/bin/activate
- run:
name: Install napari-dev
command: |
. venv/bin/activate
python -m pip install -e "napari/[pyqt5,docs]"
. .venv/bin/activate
uv pip install -e "napari/[pyqt5,docs]"
environment:
PIP_CONSTRAINT: napari/resources/constraints/constraints_py3.10_docs.txt
- run:
name: Build docs
command: |
. venv/bin/activate
. .venv/bin/activate
cd docs
xvfb-run --auto-servernum make html
environment:
Expand Down
5 changes: 4 additions & 1 deletion .github/workflows/actionlint.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,10 @@ jobs:
name: Action lint
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Checkout
uses: actions/checkout@v4
with:
persist-credentials: false
- name: Check workflow files
run: |
bash <(curl https://raw.githubusercontent.com/rhysd/actionlint/main/scripts/download-actionlint.bash)
Expand Down
62 changes: 42 additions & 20 deletions .github/workflows/build_and_deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,13 @@
# Note this workflow is also triggered by
# https://github.com/napari/napari/blob/main/.github/workflows/deploy_docs.yml when a
# commit to `main` occurs in `napari/napari`
#
# For builds:
# working directory: '/home/runner/work/docs/docs'
# docs and napari are in the same parent directory
# place docs in: '/home/runner/work/docs/docs/docs'
# place napari in: '/home/runner/work/docs/docs/napari'


name: Build & Deploy PR Docs

Expand All @@ -19,14 +26,19 @@ on:
workflow_dispatch:
inputs:
target_directory:
description: 'The directory to deploy the docs to'
description: 'Deployment directory for generated docs'
required: true
default: 'dev'

concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true

env:
FORCE_COLOR: "1"

permissions: {}

jobs:
build-and-upload:
name: Build & Upload Artifact
Expand All @@ -35,43 +47,49 @@ jobs:
- name: Clone docs repo
uses: actions/checkout@v4
with:
# place in '/home/runner/work/docs/docs/docs'
path: docs # place in a named directory
persist-credentials: false
path: docs
fetch-depth: 1

- name: Clone main repo
- name: Clone napari repo
uses: actions/checkout@v4
with:
# place in '/home/runner/work/docs/docs/napari'
path: napari # place in a named directory
persist-credentials: false
path: napari
repository: napari/napari
# ensure version metadata is proper
# needs 0 depth to get the latest versions
fetch-depth: 0

- uses: actions/setup-python@v5
- name: Set up Python
uses: astral-sh/setup-uv@v5
with:
enable-cache: true
python-version: "3.10"
cache-dependency-path: |
napari/pyproject.toml

- uses: tlambert03/setup-qt-libs@v1
- name: Set up video dependency
run: sudo apt-get update && sudo apt-get install -y ffmpeg

- name: Create a virtual environment to use for the builds
run: |
uv venv
source .venv/bin/activate

- name: Install Qt Libraries
uses: tlambert03/setup-qt-libs@v1

- name: Install Dependencies
run: |
python -m pip install --upgrade pip
python -m pip install "napari/[pyqt5, docs]"
uv pip install "napari/[pyqt5, docs]"
env:
PIP_CONSTRAINT: ${{ github.workspace }}/napari/resources/constraints/constraints_py3.10_docs.txt

- name: Testing
- name: Test installation succeeded
run: |
python -c 'import napari; print(napari.__version__)'
python -c 'import napari.layers; print(napari.layers.__doc__)'

- name: Create fallback videos
run: |
sudo apt-get update && sudo apt-get install -y ffmpeg
cd docs
make fallback-videos
run: make -C docs fallback-videos

- name: Build Docs
uses: aganders3/headless-gui@v2
Expand All @@ -80,8 +98,6 @@ jobs:
GOOGLE_CALENDAR_API_KEY: ${{ secrets.GOOGLE_CALENDAR_API_KEY }}
PIP_CONSTRAINT: ${{ github.workspace }}/napari/resources/constraints/constraints_py3.10_docs.txt
with:
# Runs in '/home/runner/work/docs/docs/docs'
# Built HTML pages in '/home/runner/work/docs/docs/docs/docs/_build/html'
run: make -C docs html
# skipping setup stops the action from running the default (tiling) window manager
# the window manager is not necessary for docs builds at this time and it was causing
Expand All @@ -95,10 +111,16 @@ jobs:
name: html
path: docs/docs/_build/html/

- name: Minimize uv cache
run: uv cache prune --ci

deploy:
name: Download & Deploy Artifact
needs: build-and-upload
runs-on: ubuntu-latest
permissions:
contents: write

# Working directory: '/home/runner/work/docs/docs'
steps:
- name: Download artifact
Expand Down
44 changes: 37 additions & 7 deletions docs/_scripts/prep_docs.py
Original file line number Diff line number Diff line change
@@ -1,27 +1,48 @@
"""ALL pre-rendering and pre-preparation of docs should occur in this file.

This script is called **before** Sphinx builds the documentation.

Note: make no assumptions about the working directory
from which this script will be called.
"""

import os
import sys
from pathlib import Path
from importlib.metadata import version
from pathlib import Path

from packaging.version import parse

from scripts_logger import setup_logger

logger = setup_logger(__name__)

# Set up paths to docs and npe2 docs source
DOCS = Path(__file__).parent.parent.absolute()
NPE = DOCS.parent.absolute() / 'npe2'
logger.debug(f"DOCS: {DOCS}")
NPE = DOCS.parent.absolute() / "npe2"
logger.debug(f"NPE: {NPE}")


def prep_npe2():
# some plugin docs live in npe2 for testing purposes
"""Preps the npe2 plugin engine prior to Sphinx docs build.

Some plugin-related docs live in the npe2 repo to simplify
plugin testing.
"""
logger.debug("Preparing npe2 plugin")
# Checks if the path to npe2 repo exist. If so, bail.
if NPE.exists():
logger.debug("NPE2 plugin already present")
return
from subprocess import check_call

npe2_version = version("npe2")

logger.debug(f"npe2 version: {npe2_version}")
check_call(f"rm -rf {NPE}".split())
logger.debug("removing NPE directory succeeded")
check_call(f"git clone https://github.com/napari/npe2 {NPE}".split())

if not parse(npe2_version).is_devrelease:
check_call(f"git -c advice.detachedHead=false checkout tags/v{npe2_version}".split(), cwd=NPE)
check_call([sys.executable, f"{NPE}/_docs/render.py", DOCS / 'plugins'])
Expand All @@ -30,10 +51,19 @@ def prep_npe2():

def main():
prep_npe2()
__import__('update_preference_docs').main()
__import__('update_event_docs').main()
__import__('update_ui_sections_docs').main()
logger.debug("Prep npe2 complete")
__import__("update_preference_docs").main()
logger.debug("update_preference_docs succeeded")
__import__("update_event_docs").main()
logger.debug("update_event_docs succeeded")
__import__("update_ui_sections_docs").main()
logger.debug("update_ui_sections_docs succeeded")


if __name__ == "__main__":
# Example usage within a script
current_script_name = os.path.basename(__file__)
# Get the name of the current script
logger = setup_logger(current_script_name)

main()
59 changes: 59 additions & 0 deletions docs/_scripts/scripts_logger.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
"""Create a logger for a directory of scripts to aid in debugging."""

import logging
import os
import sys


def setup_logger(script_name, log_directory="logs"):
"""Sets up a logger for a specific script.

Args:
script_name (str): The name of the script (e.g., "my_script.py").
log_directory (str, optional): The directory to store log files. Defaults to "logs".

Returns:
logging.Logger: A configured logger instance.
"""
# Create log directory if it doesn't exist
if not os.path.exists(log_directory):
os.makedirs(log_directory)

# Extract the script name without the extension
script_name_no_ext = os.path.splitext(script_name)[0]

# Create a logger
logger = logging.getLogger(script_name_no_ext)
logger.setLevel(logging.INFO) # Set the minimum logging level

# Create a file handler
# log_file_path = os.path.join(log_directory, f"{script_name_no_ext}.log")
# file_handler = logging.FileHandler(log_file_path)
# file_handler.setLevel(logging.DEBUG)

handler = logging.StreamHandler(sys.stdout)

# Create a formatter
formatter = logging.Formatter(
"%(asctime)s - %(name)s - %(levelname)s - %(message)s"
)
# file_handler.setFormatter(formatter)
handler.setFormatter(formatter)

# Add the file handler to the logger
# logger.addHandler(file_handler)
logger.addHandler(handler)
return logger


if __name__ == "__main__":
# Example usage within a script
current_script_name = os.path.basename(__file__)
# Get the name of the current script
logger = setup_logger(current_script_name)

logger.debug("This is a debug message.")
logger.info("This is an info message.")
logger.warning("This is a warning message.")
logger.error("This is an error message.")
logger.critical("This is a critical message.")
Loading