Impure Python Poetry devShell

The nixpkgs manual shows how it’s possible to create an impure python env, in which some python packages, e.g. matplotlib, are installed via Nix (for example, because they include C libraries that depend on glibc), whereas others are installed impurely with pip.

I’m trying to do the same, but with poetry, but this doesn’t seem to work: the dependencies installed via nix are not visible to a python interpreter run under poetry (poetry run python), as poetry run removes from sys.path the derivation containing the deps installed via nix.

Below is a repro case.

I have the following flake.nix:

{
  description = "A very basic flake";

  outputs = { self, nixpkgs }: {

    devShells.x86_64-linux.default = let pkgs = nixpkgs.legacyPackages.x86_64-linux; in
      pkgs.mkShell {
        name = "test";
        packages = with pkgs; [
          poetry

          (python3.withPackages (ps: with ps; [ matplotlib ]))
        ];
      };

  };
}

and a stock pyproject.toml:

[tool.poetry]
name = "foo"
version = "0.1.0"
description = ""
authors = ["Your Name <you@example.com>"]
readme = "README.md"

[tool.poetry.dependencies]
python = "^3.10"


[build-system]
requires = ["poetry-core"]
build-backend = "poetry.core.masonry.api"

In a nix develop shell, the following works:

$ python -c "import matplotlib"

$

whereas this fails:

$ poetry run python -c "import matplotlib"
Traceback (most recent call last):
  File "<string>", line 1, in <module>
ModuleNotFoundError: No module named 'matplotlib'

$

Looking at the output of python -m site, there’s a discrepancy:

$ python -m site
sys.path = [
    '/home/asymmetric/code/foo',
    '/nix/store/6qk2ybm2yx2dxmx9h4dikr1shjhhbpfr-python3-3.10.11/lib/python310.zip',
    '/nix/store/6qk2ybm2yx2dxmx9h4dikr1shjhhbpfr-python3-3.10.11/lib/python3.10',
    '/nix/store/6qk2ybm2yx2dxmx9h4dikr1shjhhbpfr-python3-3.10.11/lib/python3.10/lib-dynload',
    '/nix/store/6qk2ybm2yx2dxmx9h4dikr1shjhhbpfr-python3-3.10.11/lib/python3.10/site-packages',
    '/nix/store/qyy1khnfvvv6m57gkh8qijj45v3h78jy-python3-3.10.11-env/lib/python3.10/site-packages',
]
USER_BASE: '/home/asymmetric/.local' (exists)
USER_SITE: '/home/asymmetric/.local/lib/python3.10/site-packages' (doesn't exist)
ENABLE_USER_SITE: False

versus

$ poetry run python -m site
sys.path = [
    '/home/asymmetric/code/foo',
    '/nix/store/6qk2ybm2yx2dxmx9h4dikr1shjhhbpfr-python3-3.10.11/lib/python310.zip',
    '/nix/store/6qk2ybm2yx2dxmx9h4dikr1shjhhbpfr-python3-3.10.11/lib/python3.10',
    '/nix/store/6qk2ybm2yx2dxmx9h4dikr1shjhhbpfr-python3-3.10.11/lib/python3.10/lib-dynload',
    '/home/asymmetric/code/foo/.venv/lib/python3.10/site-packages',
    '/nix/store/6qk2ybm2yx2dxmx9h4dikr1shjhhbpfr-python3-3.10.11/lib/python3.10/site-packages',
]
USER_BASE: '/home/asymmetric/.local' (exists)
USER_SITE: '/home/asymmetric/.local/lib/python3.10/site-packages' (doesn't exist)
ENABLE_USER_SITE: False

Note that in the second case, the -env derivation is missing, which is the one with the Python environment containing matplotlib. (Also, poetry automatically creates a venv, but I think that’s not relevant for this problem)

Is there a way to add the missing paths to sys.path?

Looks like this is handled in sitecustomize.py.

It explictly avoids changing sys.path when we’re in a virtualenv, but then it should work when we’re using poetry but explicitly not in a virtualenv (via this option) – whereas it doesn’t, i.e. the nix-provided site-packages are still not exposed to the interpreter in that case. sys.path is changed both inside and outside a virtualenv.

I’ve also tried using the virtualenvs.options.system-site-packages Poetry option, but with no success.

This, clearly, works:

[asymmetric@tachikoma:~/code/foo]$ poetry run python
Python 3.10.11 (main, Apr  4 2023, 22:10:32) [GCC 12.3.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> import site
>>> site.addsitedir("/nix/store/qyy1khnfvvv6m57gkh8qijj45v3h78jy-python3-3.10.11-env/lib/python3.10/site-packages")
>>> import matplotlib
>>> 

But I am not sure how to ensure it’s applied to the interpreter on startup.

The problem seems to be that poetry is using a different Python than the bare shell:

# this is outside the virtualenv
[asymmetric@tachikoma:~/code/foo]$ cat $(which python)
@@@@@@@RR@@@@  @ @dd@@PX@pp@p@@@@  Sdpp@p@@Pd$$@$@,,QdRd@@((/nix/store/flf14c3ibr83jsa070j25hg5gjapydhl-glibc-2.37-8/lib/ld-linux-x86-64.so.20GNUGNU
 __libc_start_mainexecvputenvlibc.so.6GLIBC_2.2.5GLIBC_2.34/nix/store/flf14c3ibr83jsa070j25hg5gjapydhl-glibc-2.37-8/lib:/nix/store/n7pvb7gdf1g6dvj7sl92i882qjl4kyx9-gcc-12.3.0-lib/lib__gmon_start__ ui	*6@@@@@HHHt5%@%h%hH @HtU @ @%!@Ht;!@H;!@HD1HPTE11P@/..(@@H=(@@tHt	(@@f.@(@@H@@HHHtHt(@@ff.@=uUHz.]Ðf.@HHNIX_PYTHONPREFIX=/nix/store/qyy1khnfvvv6m57gkh8qijj45v3h78jy-python3-3.10.11-envNIX_PYTHONEXECUTABLE=/nix/store/qyy1khnfvvv6m57gkh8qijj45v3h78jy-python3-3.10.11-env/bin/python3.10NIX_PYTHONPATH=/nix/store/qyy1khnfvvv6m57gkh8qijj45v3h78jy-python3-3.10.11-env/lib/python3.10/site-packagesPYTHONNOUSERSITE=true/nix/store/6qk2ybm2yx2dxmx9h4dikr1shjhhbpfr-python3-3.10.11/bin/python


# ------------------------------------------------------------------------------------
# The C-code for this binary wrapper has been generated using the following command:


makeCWrapper '/nix/store/6qk2ybm2yx2dxmx9h4dikr1shjhhbpfr-python3-3.10.11/bin/python' \
    --set 'NIX_PYTHONPREFIX' '/nix/store/qyy1khnfvvv6m57gkh8qijj45v3h78jy-python3-3.10.11-env' \
    --set 'NIX_PYTHONEXECUTABLE' '/nix/store/qyy1khnfvvv6m57gkh8qijj45v3h78jy-python3-3.10.11-env/bin/python3.10' \
    --set 'NIX_PYTHONPATH' '/nix/store/qyy1khnfvvv6m57gkh8qijj45v3h78jy-python3-3.10.11-env/lib/python3.10/site-packages' \
    --set 'PYTHONNOUSERSITE' 'true'
...
[asymmetric@tachikoma:~/code/foo]$ poetry env info

Virtualenv
Python:         3.10.11
Implementation: CPython
Path:           /home/asymmetric/.cache/pypoetry/virtualenvs/foo-jKhUbUE3-py3.10
Executable:     /home/asymmetric/.cache/pypoetry/virtualenvs/foo-jKhUbUE3-py3.10/bin/python
Valid:          True

System
Platform:   linux
OS:         posix
Python:     3.10.11
Path:       /nix/store/6qk2ybm2yx2dxmx9h4dikr1shjhhbpfr-python3-3.10.11
Executable: /nix/store/6qk2ybm2yx2dxmx9h4dikr1shjhhbpfr-python3-3.10.11/bin/python3.10

Note that poetry uses the unwrapped binary. If I try to tell it to use the wrapped one, it fails with a PermissionError

[asymmetric@tachikoma:~/code/foo]$ poetry --no-ansi env use -vv  /nix/store/qyy1khnfvvv6m57gkh8qijj45v3h78jy-python3-3.10.11-env/bin

  Stack trace:

  13  /nix/store/18pwwzvq2l6sxy91s122ga2n3126s9i7-python3.10-cleo-2.0.1/lib/python3.10/site-packages/cleo/application.py:327 in run
        exit_code = self._run(io)

  12  /nix/store/y6cfz915ya0bsxnmzcm876fdxlgszz78-python3.10-poetry-1.4.2/lib/python3.10/site-packages/poetry/console/application.py:190 in _run
        exit_code: int = super()._run(io)

  11  /nix/store/18pwwzvq2l6sxy91s122ga2n3126s9i7-python3.10-cleo-2.0.1/lib/python3.10/site-packages/cleo/application.py:431 in _run
        exit_code = self._run_command(command, io)

  10  /nix/store/18pwwzvq2l6sxy91s122ga2n3126s9i7-python3.10-cleo-2.0.1/lib/python3.10/site-packages/cleo/application.py:473 in _run_command
        raise error

   9  /nix/store/18pwwzvq2l6sxy91s122ga2n3126s9i7-python3.10-cleo-2.0.1/lib/python3.10/site-packages/cleo/application.py:457 in _run_command
        exit_code = command.run(io)

   8  /nix/store/18pwwzvq2l6sxy91s122ga2n3126s9i7-python3.10-cleo-2.0.1/lib/python3.10/site-packages/cleo/commands/base_command.py:119 in run
        status_code = self.execute(io)

   7  /nix/store/18pwwzvq2l6sxy91s122ga2n3126s9i7-python3.10-cleo-2.0.1/lib/python3.10/site-packages/cleo/commands/command.py:62 in execute
        return self.handle()

   6  /nix/store/y6cfz915ya0bsxnmzcm876fdxlgszz78-python3.10-poetry-1.4.2/lib/python3.10/site-packages/poetry/console/commands/env/use.py:24 in handle
        env = manager.activate(self.argument("python"))

   5  /nix/store/y6cfz915ya0bsxnmzcm876fdxlgszz78-python3.10-poetry-1.4.2/lib/python3.10/site-packages/poetry/utils/env.py:597 in activate
        python = self._full_python_path(python)

   4  /nix/store/y6cfz915ya0bsxnmzcm876fdxlgszz78-python3.10-poetry-1.4.2/lib/python3.10/site-packages/poetry/utils/env.py:526 in _full_python_path
        subprocess.check_output(

   3  /nix/store/6qk2ybm2yx2dxmx9h4dikr1shjhhbpfr-python3-3.10.11/lib/python3.10/subprocess.py:421 in check_output
        return run(*popenargs, stdout=PIPE, timeout=timeout, check=True,

   2  /nix/store/6qk2ybm2yx2dxmx9h4dikr1shjhhbpfr-python3-3.10.11/lib/python3.10/subprocess.py:503 in run
        with Popen(*popenargs, **kwargs) as process:

   1  /nix/store/6qk2ybm2yx2dxmx9h4dikr1shjhhbpfr-python3-3.10.11/lib/python3.10/subprocess.py:971 in __init__
        self._execute_child(args, executable, preexec_fn, close_fds,

  PermissionError

  [Errno 13] Permission denied: '/nix/store/qyy1khnfvvv6m57gkh8qijj45v3h78jy-python3-3.10.11-env/bin'

  at /nix/store/6qk2ybm2yx2dxmx9h4dikr1shjhhbpfr-python3-3.10.11/lib/python3.10/subprocess.py:1863 in _execute_child
      1859│                     else:
      1860│                         err_filename = orig_executable
      1861│                     if errno_num != 0:
      1862│                         err_msg = os.strerror(errno_num)
    → 1863│                     raise child_exception_type(errno_num, err_msg, err_filename)
      1864│                 raise child_exception_type(err_msg)
      1865│ 
      1866│ 
      1867│         def _handle_exitstatus(self, sts,

The correct invocation is perhaps poetry env use /nix/store/qyy1khnfvvv6m57gkh8qijj45v3h78jy-python3-3.10.11-env/bin/python, but it has no effect:

[asymmetric@tachikoma:~/code/foo]$ poetry env use -vv  /nix/store/qyy1khnfvvv6m57gkh8qijj45v3h78jy-python3-3.10.11-env/bin/python
Trying to detect current active python executable as specified in the config.
Found: /nix/store/6qk2ybm2yx2dxmx9h4dikr1shjhhbpfr-python3-3.10.11/bin/python
Using virtualenv: /home/asymmetric/.cache/pypoetry/virtualenvs/foo-jKhUbUE3-py3.10

[asymmetric@tachikoma:~/code/foo]$ poetry env info

Virtualenv
Python:         3.10.11
Implementation: CPython
Path:           /home/asymmetric/.cache/pypoetry/virtualenvs/foo-jKhUbUE3-py3.10
Executable:     /home/asymmetric/.cache/pypoetry/virtualenvs/foo-jKhUbUE3-py3.10/bin/python
Valid:          True

System
Platform:   linux
OS:         posix
Python:     3.10.11
Path:       /nix/store/6qk2ybm2yx2dxmx9h4dikr1shjhhbpfr-python3-3.10.11
Executable: /nix/store/6qk2ybm2yx2dxmx9h4dikr1shjhhbpfr-python3-3.10.11/bin/python3.10

i.e., the python used is still the unwrapped one.