Installed custom Python script can't find it's modules

I have the following steamvr_utils/default.nix file:

{ lib
, python3Packages
, fetchFromGitHub
, daemon
}:

python3Packages.buildPythonPackage rec {
  pname = "steamvr_utils";
  version = "1.1.2"; # This is currently master but hopefully it'll be tagged at some point
  format = "pyproject";

  src = fetchFromGitHub {
    owner = "DavidRisch";
    repo = "steamvr_utils";
    rev = "master";
    sha256 = "1HQiO8JlY170WeWkD8dUwoh6OaH1SL7Gff07SvrrYew=";
  };

  nativeBuildInputs = with python3Packages; [
    hatchling
    hatch-vcs
  ];

  env.SETUPTOOLS_SCM_PRETEND_VERSION = version;

  propagatedBuildInputs = with python3Packages; [
    bluepy
    psutil
  ];

  preBuild = ''
      cat > pyproject.toml << EOF
      [build-system]
      requires = ["hatchling"]
      build-backend = "hatchling.build"

      [project]
      name = "steamvr_utils"
      version = "${version}"

      dependencies = [
          "bluepy",
          "psutil",
      ]

      [project.scripts]
      steamvr_utils = "scripts.steamvr_utils:main"
      EOF
    '';
}

The main issue is when I run steamvr_utils I get

ModuleNotFoundError: No module named 'audio'

Itā€™s failing on line 6 of scripts/steamvr_utils.py which is import audio. Iā€™m not too familiar with Python but this appears to be a local folder in the scripts folder containing a bunch of .py files. So I donā€™t think itā€™s finding them. The project isnā€™t following standard build conventions as there was no setup.py or pyproject.toml which is why I needed to add one but I assume scripts/audio/__init__.py would be used to import the audio module/folder. Iā€™m not sure how to make it find that folder though.

Have a look at this thread Broken python path in custom python package it might hold the answer; though I tried what is suggested without success. Youā€™ll need to tweak it somehow

I am trying to figure out if that package is compatible with hatchlingā€™s expectations as far as what directories the code is in. Any thoughts on that?

I am new to hatchling, but is there a chance you need to explicitly include scripts directory?

include = [
  "scripts/*.py"
]

Or you could use the setuptools backend, specifying where = ["scripts"]

That package is put together in aā€¦ letā€™s say ā€œnontraditionalā€ manner for a Python package.

Hi, Iā€™m a also new to this but interested in Python in general, here is a comparison to pyproject.toml (pip).

Thanks everyone, I think Iā€™m stuck on the entrypoint bit now. Probably more of a Python related issue.

I have:

     [project.scripts]
      steamvr_utils = "steamvr_utils.steamvr_utils:main"

in my pyproject.toml but Iā€™m getting:

  File "/nix/store/ps2rxp4p2ni45ghf8c3pd7z3gm8sqfa8-python3.11-steamvr_utils-1.1.3c/bin/.steamvr_utils-wrapped", line 6, in <module>
    from steamvr_utils.steamvr_utils import main
ModuleNotFoundError: No module named 'steamvr_utils'

Iā€™ve created a fork with the following structure:

  • steamvr_utils/
    • src/
      • steamvr_utils/ ā† I understood this is a package
        • audio/
        • basestation_interface/
        • pactl_interface/
        • steamvr_utils.py ā† and this is a module containing def main()

My understanding is that [project.scripts] format is exe_name = "package_name.module_name:main".

Any ideas what Iā€™m doing wrong?

Wow! That was a tough one! Every time I think Iā€™ve setup the hardest NixOS derivation I get a harder one!

Thanks to the comments above and a lot of searching and trial and error I eventually got to the bottom of the problem.

src

First, I had set up a local src dir to make debugging the issue easier. However, ../steamvr_utils didnā€™t seem to detect changes Iā€™d made to the project so I had to keep nudging the version number to rebuild the NixOS.
This was fixed by using the following. Iā€™m not exactly sure what this does (other than most likely ignores files in .gitignore as part of rebuild changes) but it meant I could update the source without nudging the version.

src = pkgs.nix-gitignore.gitignoreSource [ ] /data/code/steamvr_utils;

setuptools

I switched to setuptools. Some docs I was following was using setuptools and I didnā€™t have any investment in Hatchling so I just switched to that.

nativeBuildInputs = with python3Packages; [
    setuptools
    wrapPython
  ];

wrapper

I added a wrapper as per some of the suggestions above (thanks!). Not sure all of this is necessary, Iā€™ll update once Iā€™ve tested a bit more:

  postFixup = ''
    wrapProgram "$out/bin/steamvr_utils" \
      --prefix PYTHONPATH : "$PYTHONPATH" \
      --prefix PYTHONPATH : "$out/${python311.sitePackages}" \
      --prefix PATH : "${python311}/bin"
  '';

bluepy

steamvr_utils uses bluepy which needs setcap run on the bluepy-helper.

  security.wrappers.bluepy-helper = {
    owner = "root";
    group = "root";
    capabilities = "cap_net_raw,cap_net_admin+eip";
    source = "${pkgs.python3Packages.bluepy}/${pkgs.python311.sitePackages}/bluepy/bluepy-helper";
  };

This also meant Iā€™d need to tweak the path that the bluepy library uses to the bluepy-helper so instead of pointing to a relative path it points to the wrapped bluepy-helper in /run/wrappers/. If anyone knows of a nicer way to do this Iā€™m all ears (perhaps through patching?). Relevant change in the bluepy library (Iā€™ll be making a fork of this unless a better solution comes up):

diff --git a/bluepy/btle.py b/bluepy/btle.py
index 3fa3564..dff6c08 100755
--- a/bluepy/btle.py
+++ b/bluepy/btle.py
@@ -21,7 +21,7 @@ def preexec_function():

 Debugging = False
 script_path = os.path.join(os.path.abspath(os.path.dirname(__file__)))
-helperExe = os.path.join(script_path, "bluepy-helper")
+helperExe = os.path.join("/run/wrappers/bin/bluepy-helper")

restructuring of steamvr_utils

What I discovered here (and the main issue I was having) was that the script files werenā€™t present in the store once itā€™d been built.

I eventually found that adding packages in the pyproject.toml got them into the output:

[tool.setuptools]
packages = [
  "src",
  "src.config",
  "src.images",
  "src.steamvr_utils",
  "src.steamvr_utils.audio",
  "src.steamvr_utils.basestation_interface",
  "src.steamvr_utils.pactl_interface",
]

Various folders needed __init__.py files or they also didnā€™t get included.

Some assets needed adding to a MANIFEST.in file in the root of the project alongside pyproject.toml.

include src/config/*.yaml
include src/images/*.png

I can probably pull these out of src/ but Iā€™m going to tidy things up a bit generally now that I have a better idea of whatā€™s going on.

There are a few other bits which Iā€™m still working out (like whether __init__.py actually requires the imports or not. I might also go back and see if I can now configure it with the existing repo (rather than my fork) now I have a better understanding.

[project.scripts] what finally worked

Once I had the actually scripts in the store I finally understood what went into [project.scripts]:

[project.scripts]
steamvr_utils = "src.steamvr_utils.steamvr_utils:main"

Iā€™ll update this post once Iā€™ve cleaned things up a bit.

Thanks again all.

3 Likes