The Coding Lab
<- All posts

Publishing a Pure-Python CLI with Conda - 9/9

Deciding when a conda package adds value, writing a noarch recipe, testing it, and publishing it without unnecessary platform conversion.

PyPI is the natural distribution channel for a pure-Python CLI. A conda package adds value when the intended users already manage complete environments through conda or when a project depends on software outside the Python package ecosystem.

tinyHTTPie does not require compiled libraries, so conda is not technically necessary. It is still a useful final step because it shows how the same application metadata is translated into another package ecosystem—and where duplication can introduce inconsistencies.

Series

  1. From a Script to a Command-Line Tool
  2. When sys.argv Stops Scaling
  3. Designing a CLI with argparse
  4. Why a Bash Installer Becomes a Liability
  5. Packaging a Python CLI with Console Scripts
  6. Testing a Python CLI and Automating CI
  7. Documentation as Part of the Product
  8. Building a Reliable PyPI Release Pipeline
  9. Publishing a Pure-Python CLI with Conda

Decide whether the package is platform-specific

The application contains only Python code and uses Requests, which is also platform-independent at this level. Building separate Linux, macOS, Windows, and Python-version archives would duplicate the same files.

Conda represents this with noarch: python. One package can be installed across supported platforms, and conda creates the platform-appropriate command entry point during installation.

Write a focused recipe

Place the recipe in conda.recipe/meta.yaml next to the Python project:

{% set version = "0.1.0" %}

package:
  name: tihttp
  version: {{ version }}

source:
  path: ..

build:
  number: 0
  noarch: python
  script: {{ PYTHON }} -m pip install . -vv --no-deps --no-build-isolation
  entry_points:
    - tihttp = tihttp.cli:run

requirements:
  host:
    - python >=3.6
    - pip
    - setuptools
    - wheel
  run:
    - python >=3.6
    - requests >=2.21

test:
  imports:
    - tihttp
  commands:
    - tihttp --help

about:
  summary: A small command-line HTTP client
  license: MIT
  license_file: LICENSE

The dependency sections describe different environments:

  • host contains tools and libraries needed to build or install the package for its target environment.
  • run contains dependencies that must be present when a user executes tihttp.
  • test verifies both the importable package and the generated command.

For a released recipe, I would normally build from the published source archive and verify its sha256 digest rather than use a local path. The local path keeps this lifecycle example focused on the recipe itself.

Build and inspect the output

Build the recipe and ask conda-build for the resulting artifact path:

$ conda build conda.recipe
$ conda build conda.recipe --output
/path/to/conda-bld/noarch/tihttp-0.1.0-py_0.tar.bz2

The exact output directory depends on the conda installation, which is why scripts should ask conda build --output for the artifact path instead of constructing it themselves.

Because the recipe is noarch, there is no platform-conversion loop. Converting packages is not a substitute for building native binaries on their target platforms; for a pure-Python project, one architecture-independent package is the simpler representation.

Install the local artifact into a clean environment before publishing:

$ conda create --name tihttp-release \
    --channel local tihttp=0.1.0
$ conda run --name tihttp-release tihttp --help

Choose a channel and ownership model

Anaconda.org supports personal and organizational channels. With the Anaconda client authenticated, the rendered package path can be uploaded explicitly:

$ package_path="$(conda build conda.recipe --output)"
$ anaconda upload "$package_path"

For a broadly useful open-source package, conda-forge offers a different ownership model. A feedstock repository stores the recipe, automated infrastructure builds it, and community review helps maintain dependency and platform metadata. That adds process, so it should follow real user demand rather than being created only to increase the number of badges on a README.

Whichever channel we choose, metadata must remain consistent with setup.py: name, version, license, Python requirement, runtime dependencies, and entry point. Duplicated metadata is a maintenance boundary and deserves a release check.

The completed lifecycle

The series began with one hard-coded request. The same small program now has:

  • a defined command-line interface;
  • explicit failure behavior and exit codes;
  • standard package metadata and a generated command;
  • deterministic tests and a support matrix;
  • user and maintainer documentation;
  • traceable PyPI release artifacts;
  • an optional conda distribution for environment-oriented users.

The code did not need to become large. Most of the evolution happened at its boundaries. That is the central lesson: turning a script into a tool means making its contracts explicit enough that people and other tools can rely on it.

Further reading

<< section 8