How do I expose a temporary, hot-reloadable python app via nix-shell

When I run nix-shell against the following shell.nix, I’m provided with a temporary executable where changes made to the source code are immediately reflected:

$ cat shell.nix
{ pkgs ? import <nixpkgs> {} }:
pkgs.python38Packages.buildPythonPackage rec {
  name = "my-project";
  src = ./my_project;
  propagatedBuildInputs = with pkgs.python38Packages; [ pycryptodome setuptools ];
}
$ nix-shell shell.nix
# lots of output including the stack trace described by:
# https://discourse.nixos.org/t/pip-not-found-by-python39-in-nix-shell/24017/5
[nix-shell] $ my-package
hello
[nix-shell] $ vim file.py # update the string printed
[nix-shell] $ my-package
hello world
[nix-shell]$ which my-project
/tmp/tmp.m0JwP7xpRS/bin/my-project
[nix-shell]$ realpath /tmp/tmp.m0JwP7xpRS/bin/my-project 
/private/tmp/tmp.m0JwP7xpRS/bin/my-project

This is working fine for development except for an ugly stack trace due to python not being able to find pip. But it doesn’t seem like the correct way to do it because it’s not using mkShell (and throws a low-sev error). When I update the shell.nix to use mkShell, I get the stack trace show below:

$ cat shell.nix
{ pkgs ? import <nixpkgs> {} }:
pkgs.mkShell {
  packages = [
    (pkgs.python38Packages.buildPythonPackage rec {
      name = "my-project";
      src = ./my_project;
      propagatedBuildInputs = with pkgs.python38Packages; [ pycryptodome setuptools ];
    })
  ];
}
$ nix-shell shell.nix
# some healthy build output then:
Executing setuptoolsBuildPhase
Traceback (most recent call last):
  File "nix_run_setup", line 3, in <module>
    import setuptools
  File "/nix/store/cflil6pns2v38j68ifgzax6aaz2k82qr-python3.8-setuptools-65.3.0/lib/python3.8/site-packages/setuptools/__init__.py", line 16, in <module>
    import setuptools.version
  File "/nix/store/cflil6pns2v38j68ifgzax6aaz2k82qr-python3.8-setuptools-65.3.0/lib/python3.8/site-packages/setuptools/version.py", line 1, in <module>
    import pkg_resources
  File "/nix/store/cflil6pns2v38j68ifgzax6aaz2k82qr-python3.8-setuptools-65.3.0/lib/python3.8/site-packages/pkg_resources/__init__.py", line 969, in <module>
    class Environment:
  File "/nix/store/cflil6pns2v38j68ifgzax6aaz2k82qr-python3.8-setuptools-65.3.0/lib/python3.8/site-packages/pkg_resources/__init__.py", line 973, in Environment
    self, search_path=None, platform=get_supported_platform(),
  File "/nix/store/cflil6pns2v38j68ifgzax6aaz2k82qr-python3.8-setuptools-65.3.0/lib/python3.8/site-packages/pkg_resources/__init__.py", line 191, in get_supported_platform
    plat = 'macosx-%s-%s' % ('.'.join(_macos_vers()[:2]), m.group(3))
  File "/nix/store/cflil6pns2v38j68ifgzax6aaz2k82qr-python3.8-setuptools-65.3.0/lib/python3.8/site-packages/pkg_resources/__init__.py", line 366, in _macos_vers
    version = platform.mac_ver()[0]
AttributeError: module 'platform' has no attribute 'mac_ver'
error: builder for '/nix/store/wvi05r8alvdqqki0skyicx67kxif35vx-python3.8-my-project.drv' failed with exit code 1;
       last 10 log lines:
       >     import pkg_resources
       >   File "/nix/store/cflil6pns2v38j68ifgzax6aaz2k82qr-python3.8-setuptools-65.3.0/lib/python3.8/site-packages/pkg_resources/__init__.py", line 969, in <module>
       >     class Environment:
       >   File "/nix/store/cflil6pns2v38j68ifgzax6aaz2k82qr-python3.8-setuptools-65.3.0/lib/python3.8/site-packages/pkg_resources/__init__.py", line 973, in Environment
       >     self, search_path=None, platform=get_supported_platform(),
       >   File "/nix/store/cflil6pns2v38j68ifgzax6aaz2k82qr-python3.8-setuptools-65.3.0/lib/python3.8/site-packages/pkg_resources/__init__.py", line 191, in get_supported_platform
       >     plat = 'macosx-%s-%s' % ('.'.join(_macos_vers()[:2]), m.group(3))
       >   File "/nix/store/cflil6pns2v38j68ifgzax6aaz2k82qr-python3.8-setuptools-65.3.0/lib/python3.8/site-packages/pkg_resources/__init__.py", line 366, in _macos_vers
       >     version = platform.mac_ver()[0]
       > AttributeError: module 'platform' has no attribute 'mac_ver'
       For full logs, run 'nix log /nix/store/wvi05r8alvdqqki0skyicx67kxif35vx-python3.8-my-project.drv'.
error: Recipe `shell` failed on line 3 with exit code 1

I can update the python package to be a proper derivation and it will build but it will not hot-reload the executable when the source changes, so is not useful for quick tweak/checks while developing:

$ cat shell.nix
{ pkgs ? import <nixpkgs> {} }:
pkgs.mkShell {
  packages = [
    (pkgs.python38Packages.buildPythonPackage rec {
      pname = "my-project";
      version = "0.0.1";
      format = "setuptools";
      src = ./.;
      propagatedBuildInputs = with pkgs.python38Packages; [ pycryptodome setuptools ];
      doCheck = false;
      pythonRelaxDeps = true;
    })
  ];
}
$ nix-shell shell.nix
# lots of healthy build output including zero errors
[nix-shell] $ my-package
hello
[nix-shell] $ vim file.py # update the string printed
[nix-shell] $ my-package
hello # update not reflected :(
[nix-shell]$ which my-project
/nix/store/abc123-python3.8-my-project-0.0.1/bin/my-project

A few questions:

  • what exactly is causing the first example to expose an executable from a private tmp dir?
  • how can I expose a hot-reloadable executable while still using mkShell?
  • how could I tweak the first example to fix the stack trace while preserving the nice tweak/check cycle it enables?
  • Is there any other guidance you could share? I’d appreciate relevant links to docs or source code because I’m confused by what’s going on here.

Thank you in advance!