I have a Python component that I use in multiple GH files. Every time I update the component to fix a bug or whatever, I need to (somewhat laboriously) go to each GH file that uses it and update it to use the new version.
This isn’t a show stopper, just a bit of a pain. But like any programmer I worry (especially when trying to sleep at night) about source control…did I remember to update all the right files?
Is there a cleaner/more reliable way to do this? Can I somehow call the component in the “master” GH file from another file? Convert it to a plugin, perhaps?
It is possible to load a Python script from an external file to simplify the process of updating your Python component across several GH files. Here’s how to do it:
I have a script that can distribute the latest C# code in one GH definition to other GH definitions that contain C# nodes with the same name. I don’t know the same approach works for Python, but should be the same principle.
//Inputs: file (string), name (List<string>), target (List<string>)
var io = new Grasshopper.Kernel.GH_DocumentIO();
io.Open(file);
string dir = System.IO.Path.GetDirectoryName(file);
var doc = io.Document;
doc.Enabled = true;
Dictionary<string,Tuple<string,string>> dat = new Dictionary<string,Tuple<string,string>>();
foreach(var obj in doc.Objects )
{
if(obj.GetType().ToString().Contains("CSNET_Script"))
{
if(name.Contains(obj.NickName))
{
int index = name.IndexOf(obj.NickName);
string tt = target[index];
var _source = Grasshopper.Utility.InvokeGetterSafe(obj, "ScriptSource");
string source = (string) Grasshopper.Utility.InvokeGetterSafe(_source, "ScriptCode");
string additionalsource = (string) Grasshopper.Utility.InvokeGetterSafe(_source, "AdditionalCode");
dat[tt] = new Tuple<string,string>(source, additionalsource);
}
}
}
var ff = System.IO.Directory.EnumerateFiles(dir, "*.gh");
foreach(var f in ff)
{
var _io = new Grasshopper.Kernel.GH_DocumentIO();
_io.Open(f);
var _doc = _io.Document;
_doc.Enabled = true;
foreach(var obj in _doc.Objects )
{
if(obj.GetType().ToString().Contains("CSNET_Script"))
{
if(target.Contains(obj.NickName))
{
var elem = dat[obj.NickName];
var source = elem.Item1;
var additionalsource = elem.Item2;
var _source = Grasshopper.Utility.InvokeGetterSafe(obj, "ScriptSource");
Grasshopper.Utility.InvokeSetterSafe(_source, "ScriptCode", source);
Grasshopper.Utility.InvokeSetterSafe(_source, "AdditionalCode", additionalsource);
_io.Save();
}
}
}
}
It depends a bit on whether you’re referring to the existing GHPython component or the CPython/IronPython modes of the new Rhino 8 Grasshopper script editor. And on whether or not this is just for you or for many users (and if those users share a server etc.). Either way, in addition to the methods proposed above, here are some options to consider:
1) The most general, simple and Pythonic approach that should work no matter what:
Structuring all the relevant classes and functions into a Python module and importing/calling these within your component. Meaning that you would just need to update a Python file that lives locally or on a server. Here’s a slide from the in-house Python course I teach demonstrating this:
2) Automating reading and overwriting the component code:
The GHPython component (and presumably the new script editor component) has a Code property, which one can systemically read/overwrite in order to update the code within the component. Meaning that one can develop functions that both “push” and “pulls” code from a central folder structure on e.g. a server, a DropBox, or a GitHub. I’ve used this approach quite extensively in my old research groups, when running workshops, and in-house at BIG. Have a look at the two first code blocks for some examples:
3) Compiling GHPython components to a plugin:
I have never had much luck with and find the workflow pretty cumbersome, but it might work for your needs:
4) Using the plugin options provided by the new script editor:
Again I have not used these new features, but they might be relevant to your goals:
I have had good luck with @AndersDeleuran 's solution #1 (packaging and importing python modules) for plugins. However, just to add a few small personal preference items in case its helpful:
I prefer to separate the actual source code and the modules that Rhino is accessing. This makes it easier for me to create git repository for the package, include tests, virtual-environments, manage package versions, include docs, experiment, etc… without having all of that right in the Rhino scripts directory, or messing with the path in Rhino to point it at some other folder. But in order to shorten the iteration time (ie: make a change in the code, test the code in Rhino, …) I use a free VSCode plugin called ‘fsdeploy’ which watches and copies changed files into the deployment directory as I am editing them. This helps make it much easier to write and then test the edited code in Rhino.
Note that one big plus to writing in an editor like VSCode is that if you type-hint your functions and methods using the Python 2.7 MyPy style and also use the Rhino-stubs + Grasshopper-stubs, it will recognize the types and you get nice autocomplete and type-checking, if you like that kind of thing.
When writing code and testing in Rhino, using the python ‘reload’ function on your imported module can help shorten the development cycle a lot as well. This reloads the module every GH-component execution, so you can see any newly written / edited code working without rebooting Rhino.
There are some ‘gotchas’ with reload however, especially if you or any of the packages you interact with use isinstance checks - so I usually put the reload under a ‘Dev-mode’ flag, so I can turn it on to write and test, then turn if off to deploy.
As just a general suggestion, I have been very happy with the pattern where the GH-component functions as only a facade/wrapper to collect the inputs, and display user-messages, but passes off all the actual work to an outside function or, as I mostly prefer, a class for each component. All input validation, work, and output filtering/sorting happen in this module outside GH. The less ‘real’ code in the GH-Component, the easier it is to manage versions and updates, IMO.
Can someone please make this easy for those of us who don’t want to know the entire Python “stack”?
I’m looking for the equivalent of a simple include file to reuse a bit of code in multiple Python components in a GH file. So far, I see no way to specify a path when importing a module? Where does it look? Or how do I tell it where to look?
No reply… I’m not sure if that’s because few people know the answer or because the few who do are offended by my use of the word “simple” in my question?
Somewhere on this page:
I got the idea that locating the module file in the same directory as the GH file might work. So I tried that using the fibo.py example on this page:
# Fibonacci numbers module
def fib(n): # write Fibonacci series up to n
a, b = 0, 1
while a < n:
print(a, end=' ')
a, b = b, a+b
print()
def fib2(n): # return Fibonacci series up to n
result = []
a, b = 0, 1
while a < n:
result.append(a)
a, b = b, a+b
return result
But that produced this mysterious error:
Runtime error (SyntaxErrorException): unexpected token '='
Traceback:
line 1, in script
Which was confusing… and made sense only when I pasted the entire fibo.py file into my Python GH component, which generated a more informative error message:
So without bothering to figure out why this statement fails: print(a, end=' '), I simply deleted the first def fib(n) and left only def fib2(n) in the fibo.py file. So my GH Python component has this:
import fibo
a = fibo.fib2(x)
And the fibo.py file in the same folder has this:
# Fibonacci numbers module
def fib2(n): # return Fibonacci series up to n
result = []
a, b = 0, 1
while a < n:
result.append(a)
a, b = b, a+b
return result
You can have your python scripts in a random folder and import it into several grasshopper scripts. Only editing 1 file is necessary. Let me know if you need additional help.
But C:\Program Files\Rhino 8\System\netcore has no \IronPython.dll\Lib\site-packages, only a \runtimes folder with no distutils-precedence.pth file anywhere.
Trying ‘Python 3 Script’ instead (Py3) causes Rhino to freeze so hard I have to kill it. Multiple tries, all the same result. Completely useless. R8 has been “official” for a long time, yet is still infested with bugs.
P.S. In this folder: C:\Program Files\Rhino 8\System - among many other files - I find these two:
I’ll spare you the gory details I went through to update R8. It was a dysfunctional mess, far from automatic. If McNeel doesn’t already know about these difficulties, they should.
Finally I managed to download rhino_en-us_8.5.24072.13001.exe and execute it. Py3 in R8 worked this time and showed this when I ran your `for sp in sps: loop:
I re-booted my computer and tried again. Failed again. I’m done for now banging my head against this wall. What I got yesterday in R7 works fine for me. Thanks anyway, it was a wild goose chase.
I couldn’t get this to work (putting the module in the same directory as the Grasshopper file). Maybe because I’m on a Mac? Too bad, what a nice simple solution that would be. I guess I could mess with path variables somewhere in the Rhino files…but nah
Putting the module in the scripts folder, as AndersDeleuran suggested, seems to work fine. It’s still a bit of a pain - there are separate scripts folders for different Rhino versions, and it makes it tough to keep the module in GitHub with all the grasshopper scripts that use it - but I’ll muddle through.
I’m happy that others are trying to do the same thing, and thanks a bunch, all of you, for posting your thoughts. I’ll experiment a bit with aliases to see if I can keep the code centralized, but even if not, I think I’m in pretty good shape.