Distribute custom Python module with YAK

Hello there!

I have a small Grasshopper plugin (Rhino8, Windows) that I would like to distribute with YAK. Inside I have a bunch of .ghuser objects where I call a custom module (from my_module import this, that, that).

Is it possible to distribute the python module with YAK and add it to the system path so that it is available from the gh?

Many thanks in advance! :open_hands: :wave:

PS: I saw there is a new shiny tutorial for python in Rhino8 (thanks btw!), but it is unclear to me how I could for example automatize the # env: path-to-install-module, especially with yak.

Youā€™re right - I donā€™t think you can dynamically use the #env mechanism (at least not without dynamically generating the source code on the userā€™s machine). Itā€™s built into hard coded comments after all.

It is possible to import a python package shipped with Yak in RhinoPython however, (by inspecting the C# assembly, boilerplate or config files, I forget which) and figure out which PATH Yak puts your Python library on.

Then use the standard (for Rhino 7) sys.path technique:

import sys
PATH = r'c:\...\'
if PATH not in sys.path:
   sys.path.append(PATH)

In RhinoPython the entry point script is fine wherever Yak puts it. So Python libraries shipped with it can just live in the same dir. From my notes:
"make a manifest.yml with Yak (yak spec)
build package with Yak (yak build). Yak will include all sub folders, e.g. /misc and /python_packages
"

So in Grasshopper youā€™ll need to double check where Yak puts the .ghuser files, and if the path of yak installed .ghusers is in sys.path in GhPython.

I recommend investigating RhinoScriptCompiler.exe too, for packaging for Yak and setting the guid and metadata. I suspect it all works a lot more smoothly if you use C# and the Visual Studio templates.

Hello @James_Parrott , thank you for your answer! So given that YAK is able to install a folder with python modules (if it is so, I still have doubts but I will try) in the local Rhino directories, with your solution there should always be a component in the canvas that runs the code you propose to add the module to the path, right?

Another problem is the versioning and conflict problem with different modules imported and added with the method sys.path.append(PATH). In fact, although the yak manager keeps versions (e.g. 1.0.0, 1.0.1, etc), I suspect some conflicts with the previous versions of the package. But Iā€™ll give it a go!

Hi Andrea,

Youā€™re welcome Figuring out the version and the guid was the reason I tried inspecting the .Net assembly once installed.
I never got Yak-distribution of a Grasshopper plug-in with a bundled python module to work (only a Rhino one), but yes your users will need to place one of your the plug-inā€™s components on the canvas.

Iā€™m not sure why thereā€™s a hard requirement for yak, but figuring out the path to the python moduleā€™s folder is straightforward, if unzipping the plug-in into a certain known folder is made part of the user installation instructions. From within your components, you can e.g. look at the folders in:

import Grasshopper
print(vars(Grasshopper.Folders))

Within my Rhino plug-in, I added the path using this:


# https://discourse.mcneel.com/t/rhino-script-compiler-and-building-python-based-plugin-on-macos/128717/9?
this_plugin_version_path = os.path.dirname(Rhino.PlugIns.PlugIn.PathFromName("PLUG_IN_NAME")) 


appended_to_sys_dot_path = False

if this_plugin_version_path not in sys.path:
    sys.path.append(this_plugin_version_path)
    appended_to_sys_dot_path = True


try:
    import iron_pylightxl
except ImportError:
    print('Error when importing iron_pylightxl')


if appended_to_sys_dot_path:
    sys.path.remove(this_plugin_version_path)

I havenā€™t tried it for importing a Python module in Grasshopper (as I just used Grasshopper.Folders.DefaultUserObjectFolder / PLUG_IN_NAME) but if itā€™s installed with Yak, you can find all the component objects on the ribbon from external Grasshopper plug-ins (and some others) using:

gh_comp_server = Grasshopper.Kernel.GH_ComponentServer()
for file_ in gh_comp_server.ExternalFiles(True, True):
    print('Name: %s, Path: %s' % (file_.FileName, file_.FilePath))

So your components just need to search those for themselves, using the plug-in name and/or the componentā€™s own .NickName

I might have ensured the build files were within a folder with the name PLUG_IN_NAME. There were at least 12 steps in total to build the plug-in for Yak, and I never found a way to automate or script the steps with RhinoScriptCompiler.exe (they had to be done manually) so I really donā€™t recommend using Yak to distribute Python packages. Let me know if I missed something or if you find a better way.

Itā€™s far easier to package a pure python module for PyPi.
Then with Rhino 8 itā€™s far far easier to install your own module from PyPi using the fantastic # r:

1 Like

Hello @James_Parrott ! Thank you for all the info and your take on the Rhino python module. Super interesting.

I am trying now to use PyPi to distribute my module and the ghuser componentizer from Compas to create the ghusers. It makes much more sense using PyPi and the user will only use the yak installer and as you said (no manual installations), the ā€œmagicā€ r: will take care of updating to the latest version of the package.

Now the biggest problem I have with this method is how to debug and test locally without polluting the PyPi repository with thousands of minor versions just to test a ghuser component. Especially that we are a bunch working on the same project.

Thanks again for your nice insights!

1 Like

Awesome. If a module is already installed, I donā€™t think the magic # r: checks its origin or updates it to the latest version (or does it?).

So during developement and testing, you could push to TestPyPi, and in your launch code on your dev machine, run pip from the run times in %userdata%\.rhinocode\py39-rh8 (pip is in \Scripts), with the command line option to install from TestPyPi, and then launch Grasshopper or your test runner afterwards to give it a whirl.

1 Like

Indeed, you are right I think it needs to be specified (e.g. r: mypacakgename==0.0.2), so this means that before generate the components the py source code in the components should be updated with the right release version (i.e. something that would parse files, check the first lines and update the version).

Great for the TestPyPi, but if we are many developing, this would not work right? The package should be installed locally. Right?

Yes I saw there are a bunch of pip executable (pip.exe, pip3.9.exe, pip3.10.exe, pip.exe) but my first doubt is that how to point to the right python interpreter that Rhino is using?
I am sorry but itā€™s my first time handling python env management.

Great for the TestPyPi, but if we are many developing, this would not work right? The package should be installed locally. Right?

Absolutely. I forgot about editable installs - theyā€™re perfect for this. pip install -e

Re: all the variety of pips. I never looked into it, but I donā€™t think they matter? I assumed theyā€™re all aliases for the same pip. They ones with versions just allow you to be more specific about which Python installation you want to install into (instead of chancing that whichever Python is found first on the system Path is the intended one). python -m pip installs into whichever Python python opens.

1 Like

Thanks a lot @James_Parrott for sharing your knowledge, itā€™s very helpful to figruing things out.

Amazing for the editable mode for pip!

I did try to install the package with:

 C:\Users\andre\.rhinocode\py39-rh8\python.exe -m pip install <my-package-name>

And the package ends up to be installed in the directory: C:\Users\andre\.rhinocode\py39-rh8\Lib\site-packages and it is well detected in the component.

Now when the package is installed and specified from code with # r: m-package the component seems to install again the package and not detecting the previously installed one.

the package is installed in a different directory than the previous one. All the packages installed with # r ends up in: C:\Users\andre\.rhinocode\py39-rh8\site-envs\default-wMh5LZL3.

Is the latest directory some sort of virtual environment?
Also, I do not know why these two installs they do not point to the same folder. Any idea?

edit: interesting fact after some tests, if both are installed the latest will be the one loaded by the component and the first package will be discarded.

Iā€™ve just noticed the new CPython creates a folder like that (the suffix is random), only when run from GH. Deleting that folder is a quick way of uninstalling everything installed via # r:. Grasshopper will create a new one, the next time a CPython component is placed.

Isnā€™t there another magic switch, # env: ? So if the auto-magick pip checks for a pre-existing install, you could try preceding the # r: with:

# env: C:\Users\andre\.rhinocode\py39-rh8\Lib\site-packages

Otherwise there are many ways GH CPython couldā€™ve been configured, but it all depends what order the two folders are in on sys.path. As long as C:\Users\andre\.rhinocode\py39-rh8\Lib\site-packages occurs before ...\default-wMh5LZL3 in sys.path, the editable install will get imported preferentially (I think itā€™s then necessary to manipulate sys.path or build an Import Hook to import the other one).

Hopefully the extra install from PyPi isnā€™t too time consuming.

1 Like

Thanks @James_Parrott for the suggestion of # env. I think the best will be to just erase the package if exists in C:\Users\andre\.rhinocode\py39-rh8\site-envs\default-wMh5LZL3 installed with # r and just stick to the editable pip install (thank you again for the suggestion btw!).

I have a preliminary tutorial on how to develop and generate a GH plugin in python here.

Iā€™ll try to create a template-repository to have something proper.

1 Like