Vincent's Personal Page

Conda Workflow

This page describes my Conda workflow.

Conda

I use Conda as my environment and package manager. This choice is the outcome of a long professional history. Pros for choosing Conda:

Cons:

My Conda workflow is consequently an attempt at overcoming some of these issues.

Conda distro choice and configuration (ordered by preference):

  1. mambaforge.
  2. miniforge, install Mamba manually.
  3. miniconda, add conda-forge to our channels, install Mamba manually.

Living with others: A project with and without Conda

Sometimes we can’t get away with just Conda, especially when we work with others and when we start publishing software. But I still don’t want to give up on Conda, I’m very stubborn! So how can we single-source project dependencies and still pin both PyPI and Conda dependencies reliably?

Solution:

Still, conda-lock is no silver bullet: It will not pin Pip-only dependencies. We therefore might have to fight for our packages. It however turns out that for many projects, it’s alright and we can develop and test with Conda, and package with Poetry. We just need to be a little careful.

We might want to pay attention to the following points when writing our pyproject.toml:

In addition, we’ll need a minimal setup.py to setup our project. I personally like the setup.cfg workflow. This way, we can install our Python package to a newly created and configured Conda env with:

$ python setup.py develop --no-deps
setup.py
import setuptools

setuptools.setup()

setup.cfg Assumptions:

  • code is in src/;
  • package is named my_package.

Your package may actually consist of several Python packages (here, we only have my_package, but we could have more).

[metadata]
name = my_package
version = attr: my_package.__version__

[options]
# Package discovery
package_dir =
    =src
packages = find:

[options.packages.find]
where = src
include = *

Now we can glue all this with a makefile:

Makefile
ifeq ($(OS), Windows_NT)
	PLATFORM := win-64
else
	uname := $(shell sh -c 'uname 2>/dev/null || echo unknown')
	ifeq ($(uname), Darwin)
		PLATFORM := osx-64
	else ifeq ($(uname), Linux)
		PLATFORM := linux-64
	else
		@echo "Unsupported platform"
		exit 1
	endif
endif

all:
	@echo "Detected platform: $(PLATFORM)"

# Lock Poetry dependencies
poetry-lock:
	poetry lock

# Lock conda dependencies
conda-lock:
	conda-lock --file pyproject.toml \
	    --filename-template "requirements/environment-{platform}.lock" > \
	    -p $(PLATFORM)

conda-lock-all:
	conda-lock --file pyproject.toml \
	    --filename-template "requirements/environment-{platform}.lock"

# Initialise development environment
conda-init:
	conda update --file requirements/environment-$(PLATFORM).lock
	python setup.py develop --no-deps

# Shortcut for poetry and conda lock
lock: conda-lock-all poetry-lock
conda-update: conda-lock-all conda-init lock
.PHONY: poetry-lock conda-lock conda-lock-all conda-init conda-update

Now, all we need to update our lock files is a simple

$ make lock

We can initialise or update a Conda environment with

$ make conda-init

Testing with Nox

Next up on our chore list is setting up Nox in a way such that we’ll have:

Once again, single-sourcing dependencies is the big problem, and sadly we can’t solve it completely yet.

Installing Nox

The simplest and universal way is to use pipx. This one can be installed to your base Conda env or globally using Homebrew/Linuxbrew. Note that you will probably have to add pipx to your path using pipx ensurepath.

Then:

$ pipx install nox

Using pipx is nicer than using Conda because you can then customise pipx with PyPI packages — also doable with Conda, but not as cleanly. We will install the nox-poetry plugin:

$ pipx inject nox nox-poetry

Configuring Nox sessions

We can configure our Nox session and combine Poetry and Conda in various ways to cover more Python versions and test whether our package works with Conda-managed dependencies.

noxfile.py
import nox
import nox_poetry


# Virtualenv + Poetry
@nox_poetry.session(python=["3.6", "3.7", "3.8", "3.9"])
def test_poetry(session):
    session.run("poetry", "install", external=True)
    session.run("pytest")


# Conda + Poetry
@nox.session(venv_backend="conda", python=["3.6", "3.7", "3.8", "3.9"])
def test_conda_poetry(session):
    session.run("poetry", "install", external=True)
    session.run("pytest")


# Conda + Conda
@nox.session(venv_backend="conda", python=["3.6", "3.7", "3.8", "3.9"])
def test_conda_conda(session):
    session.conda_install("pytest", "setuptools")  # Add here other deps which cannot be read from pyproject.toml
    session.run("python", "setup.py", "develop", "--no-deps")
    session.run("pytest")

#conda #poetry #python