widgetti / solara

A Pure Python, React-style Framework for Scaling Your Jupyter and Web Apps
https://solara.dev
MIT License
1.91k stars 141 forks source link

Vue components unable to find template vue files when using frozen/pyinstaller application on Windows #825

Closed suhren closed 4 weeks ago

suhren commented 1 month ago

I have been using PyInstaller to create an executable .exe file for my solara application, and that has, in general, worked very well. However, recently I started using the Menu component and that caused the following issue when I was building the application to an executable using PyInstaller:

Traceback (most recent call last):
  File "reacton\core.py", line 388, in _create_widget
  File "ipyvue\VueTemplateWidget.py", line 144, in __init__
  File "solara\server\patch.py", line 250, in wrapper
  File "ipyvue\Template.py", line 47, in get_template
FileNotFoundError: [Errno 2] No such file or directory: 'solara\\lab\\components\\menu.vue'

On the other hand, the solara application was working completely fine if I ran it as a normal python program from the terminal.

I belive I have traced the problem to the component_vue decorator which in turn calls a wrapper function that uses inspect.getfile to get the path to the file where the decorated function is defined. It looks like follows:

def _widget_vue(vue_path: str, vuetify=True) -> Callable[[Callable[P, None]], Type[v.VuetifyTemplate]]:
    def decorator(func: Callable[P, None]):
        class VuetifyWidgetSolara(v.VuetifyTemplate):
            template_file = (inspect.getfile(func), vue_path)

        class VueWidgetSolara(vue.VueTemplate):
            template_file = (inspect.getfile(func), vue_path)

        base_class = VuetifyWidgetSolara if vuetify else VueWidgetSolara
        widget_class = _widget_from_signature("VueWidgetSolaraSub", base_class, func, "vue_")

        return widget_class

    return decorator

We can see here that the call inspect.getfile(func) is expected to provide the full absolute path to the file. When not using a frozen executable on Windows (or using some other platform like Mac), this works as expected, but when using the frozen executable on Windows, the inspect.getfile(func) will return a relative path, leading the the vue file not being found.

A simple solution (which I have tested already) is to surround the inspect.getfile(func) with os.path.abspath, as this will correctly resolve the path, no matter if the inspect module returns a relative path, or not.

maartenbreddels commented 1 month ago

Hi Adam,

thanks for you well described issue, and PR! Excited to see you use solara with pyinstaller! Could you elaborate on why you are using it, and for what kinda of project?

However, I don't fully understand the issue.

We can see here that the call inspect.getfile(func) is expected to provide the full absolute path to the file.

Why is that the case you think? If I look at the code path at https://github.com/widgetti/ipyvue/blob/9490c7b694414207cbb1ce8b15d993d80dda72bb/ipyvue/VueTemplateWidget.py#L140 this shouldn't be a problem.

Btw, did you see https://github.com/widgetti/solara/pull/724 ? It would be nice to include the issue you have in this PR as a test. I hope to work on it this week.

Also, I am surprised this is a problem, because this is being used in https://github.com/spacetelescope/jdaviz/ without having these issues.

Regards,

Maarten

suhren commented 1 month ago

Hi @maartenbreddels and thanks for replying. Let me elaborate on the behavior I am seeing with how inspect.getfile works on Windows together with PyInstaller. Consider the following simple test program I have put in a file as app/main.py:

import os
from inspect import getfile

def my_function():
    pass

print(getfile(my_function))
print(os.path.abspath(getfile(my_function)))
input("Press any key to exit")

If I simply run this with Python as normal, I get the following output:

$ python app/main.py
> C:\Users\suhren\test-template-vue-file-path\app\main.py
> C:\Users\suhren\test-template-vue-file-path\app\main.py
> Press any key to exit

I.e., inspect.getfile corretly gives the full path to the python file (and using os.path.abspath doesn't change the path). However, if I try building this to an executable with pyinstaller, it behaves as follows:

# Build .exe using pyinstaller
$ pyinstaller app/main.py
# Launch the built .exe
$ ./dist/main/main.exe
> main.py
> C:\Users\suhren\test-template-vue-file-path\main.py
> Press any key to exit

Now, note that the raw output from inspect.getfile now only gives the string "main.py", as opposed to the full path. Adding os.path.abspath here, fixes it and produces the full path.

I have not yet had time to look at the other "jdaviz" project, or #724, but I can get back to you on that when I have time. But I hope the example I provided in this comment explains the issue. And as mentioned, my PR in #826 fixes this issue using PyInstaller (on my Windows system at least) and makes the vue components work as expected.

maartenbreddels commented 4 weeks ago

Making the path absolute seems like a good idea, which will also make it more robust again changing current directory. See:

Thanks for the details!