Using Modules In Script Editor Code Project

Hi @eirannejad ,

I’m having a little confusion about the way to reference modules from a main script within a Script Editor Code Project.

According to the docs here:

the structure in my Code Project Libraries for my module should be like this:

└───UIThemeColors  # I guess this is the main "Module Name"?
    ├───__init__.py  # Is this everything before the GetThemeColors Function?
    └───GetThemeColors.py  # This is the function logic of GetThemeColors() saved as a 
                             seperate .py file?

where this is the full “UIThemeColors” module code below that I used to call in my main script from a local network path like this:

import sys

Add the directory containing FindAnything.py to the Python path
modules_path = r"pathtomylocalcodefolders\modules"
sys.path.append(modules_path)

from UIThemeColors import GetThemeColors

This would allow me to call the function “GetThemeColors” in my main script and get the returned values.

here’s an example of that code:

#! python3

# UIThemeColors.py

import Eto.Drawing


# Define color variables based on the theme
bg_color_light = Eto.Drawing.Color(10, 10, 10)
bg_color_dark = Eto.Drawing.Color(220, 220, 220)

mg_color_light = Eto.Drawing.Color(1, 1, 1)
mg_color_dark = Eto.Drawing.Color(240, 240, 240)

fg_color_light = Eto.Drawing.Color(1, 1, 1)
fg_color_dark = Eto.Drawing.Color(180, 180, 180)

b_color_light = Eto.Drawing.Color(40, 40, 40)
b_color_dark = Eto.Drawing.Color(240, 240, 240)

s_color_light = Eto.Drawing.Colors.LightGrey  # Shadow Color To Replace Color Highlight When In Light Mode
s_color_dark = Eto.Drawing.Color(240, 240, 240)

e_color_light = Eto.Drawing.Color(20, 20, 20)
e_color_dark = Eto.Drawing.Color(250, 250, 250)

d_color_light = fg_color_light
d_color_dark = Eto.Drawing.Color(210, 210, 210)


def GetThemeColors(dark_mode):
    if dark_mode:
        # Define theme colors for dark mode
        theme_colors = {
            "bg_color": bg_color_dark,
            "mg_color": mg_color_dark,
            "fg_color": fg_color_dark,
            "b_color": b_color_dark,
            "s_color": s_color_dark,
            "e_color": e_color_dark,
            "d_color": d_color_dark,
            "t_color": fg_color_light
        }
    else:
        # Define theme colors for light mode
        theme_colors = {
            "bg_color": bg_color_light,
            "mg_color": mg_color_light,
            "fg_color": fg_color_light,
            "b_color": b_color_light,
            "s_color": s_color_light,
            "e_color": e_color_light,
            "d_color": d_color_light,
            "t_color": fg_color_dark
        }
    return theme_colors

Do I need to split out the function GetThemeColors as it’s own .py file and put everything above that in the __init__.py file of a folder called UIThemeColors

Like so?
image

Where __init__.py becomes:

#! python3

import Eto.Drawing


# Define color variables based on the theme
bg_color_light = Eto.Drawing.Color(10, 10, 10)
bg_color_dark = Eto.Drawing.Color(220, 220, 220)

mg_color_light = Eto.Drawing.Color(1, 1, 1)
mg_color_dark = Eto.Drawing.Color(240, 240, 240)

fg_color_light = Eto.Drawing.Color(1, 1, 1)
fg_color_dark = Eto.Drawing.Color(180, 180, 180)

b_color_light = Eto.Drawing.Color(40, 40, 40)
b_color_dark = Eto.Drawing.Color(240, 240, 240)

s_color_light = Eto.Drawing.Colors.LightGrey  # Shadow Color To Replace Color Highlight When In Light Mode
s_color_dark = Eto.Drawing.Color(240, 240, 240)

e_color_light = Eto.Drawing.Color(20, 20, 20)
e_color_dark = Eto.Drawing.Color(250, 250, 250)

d_color_light = fg_color_light
d_color_dark = Eto.Drawing.Color(210, 210, 210)

and GetThemeColors.py becomes:

def GetThemeColors(dark_mode):
    if dark_mode:
        # Define theme colors for dark mode
        theme_colors = {
            "bg_color": bg_color_dark,
            "mg_color": mg_color_dark,
            "fg_color": fg_color_dark,
            "b_color": b_color_dark,
            "s_color": s_color_dark,
            "e_color": e_color_dark,
            "d_color": d_color_dark,
            "t_color": fg_color_light
        }
    else:
        # Define theme colors for light mode
        theme_colors = {
            "bg_color": bg_color_light,
            "mg_color": mg_color_light,
            "fg_color": fg_color_light,
            "b_color": b_color_light,
            "s_color": s_color_light,
            "e_color": e_color_light,
            "d_color": d_color_light,
            "t_color": fg_color_dark
        }
    return theme_colors

so if i make a new command that has a script “Checks Current Theme Colors”

within that script then I can call:
from UIThemeColors import GetThemeColors

because it “sees” that in my Libraries/ folder of the ScriptEditor code project.

Is that correct?

Thanks for the help!

EDIT:

If I try to make a new test command called “UIThemeTest” like so:

#! python3

from UIThemeColors import GetThemeColors

colors = GetThemeColors()
print(colors)

I get this error when running the command in Rhino:

Command: UIThemeTest
Traceback (most recent call last):
  File "file:///C:/Users/micha/.rhinocode/stage/ttge5hr1.aue", line 3, in <module>
ImportError: cannot import name 'GetThemeColors' from 'UIThemeColors' (unknown location)
Error occured running command "7d5ae468-2482-4170-8fbd-1d9d2ee54fbc" | Traceback (most recent call last):
  File "file:///C:/Users/micha/.rhinocode/stage/ttge5hr1.aue", line 3, in <module>
ImportError: cannot import name 'GetThemeColors' from 'UIThemeColors' (unknown location)

EDIT 2:

I decided to test on MacOS and was surprised to find that the code worked without a hitch leading me to believe this is possibly either a Windows related bug OR something to do with my windows version of Rhino having the wrong library/project paths or something.

Move all your code to GetThemeColors.py, then in __init__.py do:

from .GetThemeColors import GetThemeColors

This will make UiThemeColors a proper module.

2 Likes

Thank you so much! @nathanletwory that did the trick!

EDIT:

The same logic does not seem to work for Classes with nested functions (more on that below)

1 Like

Hi @nathanletwory,

Do I need to modify the logic for Classes that have nested functions?

Below is an example of an excerpt from a class that has many functions inside of it:

import re
import scriptcontext
import Rhino

class SearchModule:
    def __init__(self, search_limit=3, max_text_length=40, debug=False):
        self.search_text = ""

    def set_search_text(self, search_text):
        self.search_text = search_text.lower()

    def debug_print(self, message):
        if self.debug:
            print(message)

This exists in the following folder structure:
image

Here’s the __init__.py attempt:

from .SearchModule import *

is the asterik the way to approach it when there’s classes AND functions?

Thanks for the help!

EDIT:

I also tried __init__.py as:

from .SearchModule import SearchModule

This always returns below error when i try to import the module in a seperate/main script:

Traceback (most recent call last):
  File "myprojectfilepath/projects/ui/modules/MainScript.py", line 8, in <module>
ImportError: cannot import name 'SearchModule' from 'FindAnything' (unknown location)

attempt to import module in mainscript (that results in the above error ^):

from FindAnything import SearchModule

My Code Project and the modules I’m trying to create are all saved in my Documents folder local to my username. Is that the issue? Do i have to be saving all of this at the .rhinocode address outlined in these docs OR set the rhinocode path to my local Documents/Project folder?:

I imagine regular module and import rules are to be followed.

I suggest going throug 6. Modules — Python 3.9.20 documentation and 7. Simple statements — Python 3.9.20 documentation

1 Like

Thanks for the docs @nathanletwory, that’s really helpful!

I have read through them and feel as though this may be a bug still? :bug:


Take this simple code example below.
This will run fine on Mac OS but NOT on Windows, despite it being identical code.

I’m working out of “documents” folders on Windows and on MacOS and to my knowledge I have not modified paths or anything…

Here’s the project structure:

image


TestCommand calls TestModule.TestScript.TestClass.TestFunction essentially.

Here’s the full code for all the pieces.


Commands/
    TestCommand

Libraries/
     TestModule
         __init__.py
         TestScript.py

Commands/TestCommand

TestCommand.py
#! python3

from TestModule import TestScript

SetText = TestScript.TestClass  # Create Reference To Class

text = "Why does this work on MacOS but not Windows?"

result = SetText.TestFunction(text)

print(result)

Libraries/TestModule

__init__.py
#! python3

from TestModule.TestScript import TestClass
TestScript.py
#! python3

class TestClass():
    def __init__(self):
        my_class_variable = "variable text"

    def TestFunction(text):
        updated_text = f"My Special Text: {text}"
        return updated_text

The expected output in Terminal is:

My Special Text: Why does this work on MacOS but not Windows?


If you or @Gijs or @wim take this code and create a new “TestProject” in the Windows ScriptEditor and try to run the function “TestCommand” does it work for you?
For me it will never find the module on Windows but works just fine on MacOS.

Thanks for the help!

EDIT:

It may be worth noting I’m running 8.12 on Windows and 8.11 on MacOS

I just compiled the working plugin code on my Mac and deployed it to my Windows computer as a .yak package and still get the missing module error on Windows but not Mac, leading me to believe it’s not the ScriptCompiler but something wrong with my Windows .rhinocode paths or something. I’ll try resetting the .rhinocode paths to default from Rhino->Tools->Options->Advanced->RhinoCodePlugin.RootPath and setting it’s value to default even though I’ve not changed this in the past.

EDIT2:

Resetting the RhinoCodePlugin.RootPath to default had no effect.

Q: What is the error message you are getting on Windows?

Notes:

  • There is no need for #! python3 inside the module .py files. That directive only applies to the main script.

  • This is not a good pattern in python 3 for importing nested submodules and names. Use the second line instead:

# bad
from .SearchModule import SearchModule

# good
from . import SearchModule
1 Like

Thanks @eirannejad!

Testing with the method you suggest now.

The error message has been as follows:

Traceback (most recent call last):
  File "C:\Users\user_name\Documents\GitHub\project_name\projects\projects\ui\modules\MainScript.py", line 8, in <module>
ImportError: cannot import name 'SearchModule' from 'FindAnything' (unknown location)

Hmm now I am confused :smiley: Where did FindAnything show up from? It’s not in TestCommand.py, __init__.py or TestScript.py listed above :thinking:

I was at least in the screenshot in this reply: Using Modules In Script Editor Code Project - #5 by michaelvollrath

@eirannejad sorry FindAnything was the original module that i was working on that triggered me posting this as i was getting the cannot import name error with that code but I was able to reproduce the same error consistently with the TestCommand.py / TestScript.py listed above.

Again, the error happens on Windows, on Mac it finds the module just fine.

Thanks for the help!

1 Like

@michaelvollrath I just tried this on Windows with files shared above and not seeing any issues. Would you mind testing this project on yours?

TestProject.zip (3.0 KB)

1 Like

That project did work, thanks.

Maybe the issue is that I’m nesting my script too deep… here’s my previous project folder structure that’s causing issues on Windows:

image

and then inside “lib” is where my actual modules live:
image

EDIT:

Okay if I copy the folders inside lib into my main folder where “TestProject.rhproj” is located it seems to work now. So that makes sense… I’m going to test on my actual code now and not just the testmodules but it looks like it was my fault in nesting the folders too deep.

Since commands had no issue running being nested inside of “/commands” subfolder, I wrongly assumed I could do the same with the /lib path but obviously the modules are being “loaded” vs the commands being manually added python scripts so that makes sense why it wouldn’t work that way

EDIT 2:
image

FYI: after adding a library or modifying the folder structure one cannot use the delete button (trash can icon upper right) to remove these outdated entries. Is that intentional? Maybe there’s a command to purge the outdated entries?

Thanks!

Hm thanks looks like a bug. The delete button doesn’t work as in it throws an error or is not enabled?

It simply does nothing when clicked.
It works on deleting entries in Commands/ but it does not delete entries in Libraries/

1 Like

Okay. I see. I will get that fixed. Here is the YT:

RH-83762 Delete button does not remove missing libraries

In the meantime you can open the .rhproj file with a text editor and delete the library entry from the json structure.

1 Like

Great tip! Thank you for that!


Regarding the above topics I seem to have narrowed down the odd behavior on my end on Windows.

If I take the code you shared in the .zip and build it as a project and install it via drag & drop into Rhino as .rhp or .yak it successfully installs.

I can then run the TestCommand and see the expected result.

However, if I then close and re-open Rhino and try TestCommand again it will always give me the unknown location error as follows:

Command: TestCommand
Traceback (most recent call last):
  File "file:///C:/Users/micha/.rhinocode/stage/qmkximre.3pp", line 1, in <module>
ImportError: cannot import name 'TestScript' from 'TestModule' (unknown location)
Error occured running command "448816ff-466f-4a30-8581-9c72aa2da8a8" | Traceback (most recent call last):
  File "file:///C:/Users/micha/.rhinocode/stage/qmkximre.3pp", line 1, in <module>
ImportError: cannot import name 'TestScript' from 'TestModule' (unknown location)

Oh I see. I can replicate this now. The project deploys the library under a double nested path and fails to load. I’ll put a fix for this in the next 8.12 (I can also send you a new 8.13 tomorrow)

BTW I see you install .yak files into your Rhino. Make sure to cleanup the previous ones before testing new. They get installed under %APPDATA%\McNeel\Rhinoceros\packages\8.0

YT for reference: RH-83764 Python library is deployed in doubly nested folder structure

1 Like

Amazing, thank you so much! :pray:

1 Like