Nix python

From wikinotes

Projects

mach-nix https://github.com/DavHau/mach-nix
nix-direnv https://github.com/nix-community/nix-direnv

Tutorials

vanilla nix + python https://github.com/FRidh/python-on-nix/blob/master/tutorial.md
python development with nix https://thomazleite.com/posts/development-with-nix-python/
C, javascript, python, haskell managed together by nix https://matrix.ai/blog/developing-with-nix/
nix wiki: python https://nixos.wiki/wiki/Python

Package Providers

vanilla nix

NOTE:

Python's user site does separates python versions, but not package versions.
It is not safe to share packages this way.

Nix allows you to define a package for a python-version interpreter,
and set the packages you want installed to it.

This is simplest method, choosing either install packages locally, and/or using nix provided packages (less, older).

default.nix (base package)


# 'python' exposed for dependency injection in shell.nix
{ pkgs ? import <nixpkgs> {}, python ? pkgs.python38 }:

pkgs.stdenv.mkDerivation {
  name = "nixpython-1.0.0";
  buildInputs = [
    pkgs.hello
    python
    python.pkgs.pytest
    python.pkgs.pylint
    python.pkgs.bootstrapped-pip
  ];
}


shell.nix (adds development python deps to default.nix)


# shell.nix

{ pkgs ? import <nixpkgs> {}, python ? pkgs.python38 }:

let
  extra-os-pkgs = pkgs.config.vim-base;           # ex: [ curl ripgrep ... ]
  vim-python-pkgs = pkgs.config.vim-python-pkgs;  # ex: [ rope autopep8 jedi ...]
  pkg = import ./default.nix { python = python; };

  nix-python-pkgs = (map (x: builtins.getAttr x python.pkgs)
                         (builtins.filter (x: builtins.hasAttr x python.pkgs)
                                          vim-python-pkgs));
  pip-python-pkgs = (builtins.filter (x: !builtins.hasAttr x python.pkgs)
                                     vim-python-pkgs);

in
  pkg.overrideAttrs(
    old: {
      buildInputs = old.buildInputs
        ++ extra-os-pkgs
        ++ nix-python-pkgs;

      shellHook = (if builtins.hasAttr "shellHook" old
                   then builtins.getAttr "shellHook" old
                   else "")
        + "\npython -m pip install --target=.pkgs " + (builtins.concatStringsSep " " pip-python-pkgs)
        + "\nexport PYTHONPATH=$PYTHONPATH:$(pwd)/.pkgs";
    }
  )

${PACKAGE}/.envrc

use_nix

pipenv

pipenv greatly simplifies deployment.
All nix is concerned about is os packages, pipenv takes care of python and it's libs.
development is performed within the pipenv shell, which is persisted independently of the OS packages.

Injecting editor packages is less straightforwards, it is set outside of nix.

${PACKAGE}/default.nix


# Defines minimal env to run code/tests within interpreter.
# (optionally, override with your preferred packages in with a `shell.nix`)
#
# If you just want to run tests, use `python setup.py test`.
# See https://nixos.org/features.html
#
# Run:
#    nix-shell

{ pkgs ? import <nixpkgs> {} }:

pkgs.stdenv.mkDerivation {
  pname = "myproject";
  version = "1.0.0";
  buildInputs = [
    pkgs.pipenv
    pkgs.python38
  ];
}

${PACKAGE}/shell.nix


# vim: ft=nix
#
# Overrides default.nix, for inserting your own development tools.
#
# INSTRUCTIONS:
#   Copy this file to `shell.nix` and alter with desired packages.
#
# RUN:
#   nix-shell

{ pkgs ? import <nixpkgs> {} }:

let
  extra-os-packages = pkgs.config.vim-base;       # ex: [ neovim ripgrep ... ]
  vim-python-pkgs = pkgs.config.vim-python-pkgs;  # ex: [ rope autopep8 jedi ...]
  pkg = import ./default.nix {};
  install-missing-devpython-pkgs =
    ''PIPENV_INSTALLED=("$(pipenv run pip list | awk -F' ' '{ print $1 }' | tail -n +3)")''
    + pkgs.lib.strings.concatMapStrings (x: "\necho $PIPENV_INSTALLED | tr ' ' '\n' | grep '^${x}\$' || pipenv run pip install ${x}") vim-python-pkgs;

in
  pkg.overrideAttrs(
    old: {
      buildInputs = old.buildInputs ++ extra-os-packages;
      shellHook = (if builtins.hasAttr "shellHook" old
                  then builtins.getAttr "shellHook" old
                  else "")
                  + install-missing-devpython-pkgs;
    }
  )

${PACKAGE}/Pipfile

[[source]]
name = "pypi"
url = "https://pypi.org/simple"
verify_ssl = true

[dev-packages]

[packages]
recommonmark = "*"
groundwork-sphinx-theme = "*"
sphinx-markdown-tables = "*"
Sphinx = "*"

[requires]
python_version = "3.9"

${PACKAGE}/.envrc

use_nix
nix-shell       # (or use direnv hook)
pipenv install  # install requirements from Pipfile
pipenv shell    # enter pipenv


mach-nix

mach-nix is a tool that maps all python packages from pypi.org to nix packges.
It's usage is interchangeable with the workflow for vanilla nix.
It is straightforwards to wrap default.nix and inject additional requirements for your editor.

shell.nix (describe shell, not build)

# ./shell.nix

{ pkgs ? import <nixpkgs> {} }:

let
  mach-nix = import (
    builtins.fetchGit {
      url = "https://github.com/DavHau/mach-nix/";
      ref = "3.1.1";
    }
  ) { python = "python38"; };
  python-requirements = (builtins.readFile ./requirements.txt);

  customPython = mach-nix.mkPython {
    requirements = python-requirements;
  };
in

pkgs.mkShell {
  buildInputs = [ customPython ];
}

default.nix (describe build, in addition to shell)

# ./default.nix

{ pkgs ? import <nixpkgs> {} }:

let
  mach-nix = import (
    builtins.fetchGit {
      url = "https://github.com/DavHau/mach-nix/";
      ref = "3.1.1";
    }
  ) { python = "python38"; };
  customPython = mach-nix.mkPython {
    requirements = builtins.readFile ./requirements.txt;
  };

in
  pkgs.stdenv.mkDerivation rec {
    name = "nixpython";
    version = "1.0.1";
    buildInputs = [ customPython ];
  }

Interactive shell with additional python-requirements


# ./default.nix
#
# package exposing a python interpreter, with six/requests installed to it.

{ pkgs ? import <nixpkgs> {} }:

let
  mach-nix = import (
    builtins.fetchGit {
      url = "https://github.com/DavHau/mach-nix/";
      ref = "2.0.0";
    }
  );
  customPython = mach-nix.mkPython {
    python = pkgs.python38;
    requirements = ''
    six
    requirements
    ''
  };

in
  pkgs.stdenv.mkDerivation rec {
    name = "nixpython";
    version = "1.0.1";
    buildInputs = [ customPython ];
  }
# nix shell
#
# adds 'rope' package to resolved python interpreter

pkgs = import <nixpkgs> {}
pkg = import ./default.nix {}

# find 'python' package from buildInputs (if available)
python_pkgs = builtins.filter (x: builtins.hasAttr "python" x) pkg.buildInputs
python_pkg = if (builtins.length python_pkgs) > 0 then builtins.head python_pkgs else null

# assemble requirements
inputs = pkg.buildInputs
inputs = (if !(builtins.isNull python_pkg) then  inputs ++ [python_pkg.pkgs.rope] else inputs)
pkg-dev = pkgs.stdenv.mkDerivation { name="foo"; buildInputs=inputs; }
:s pkg-dev

Development Environment Notes

Nix adds python packages to your $PYTHONPATH
You'll need to add them to sys.path within vim if you use mach-nix, since the interpreter is not selected by default in vim.

# ~/.vimrc
if has("unix") && ($IN_NIX_SHELL != "")
    " vanilla nix packages are separate PYTHONPATH entries
    execute "python3 import sys;sys.path = '".$PYTHONPATH."'.split(\":\") + sys.path"

    " mach-nix packages use alt/2nd python interpreter. Add it's packages to PYTHONPATH
    let sitepackages = system("python3 -c 'import site;print(site.getsitepackages()[0])'")
    execute "python3 import sys;sys.path.insert(0, '" . trim(sitepackages) .  "')"
end