I'd like to import internal exe modules from an external process. I can do this fine from an IDE, but when I package the project with pyinstaller, then run it, the modules can't be found.
The high level flow looks like this:
Importing maya_app from within the external userSetup.py returns an error: ImportError: No modules named maya_app
This post seems like it should help me but there's something about my setup that's preventing this solution from working. That or I'm just doing it wrong.
Is it even possible to import modules from one exe into another exe? Thanks!
Code:
launch_maya.py
import os
import sys
import subprocess
from PySide2 import QtWidgets
class Widget(QtWidgets.QWidget):
def __init__(self, parent=None):
super(Widget, self).__init__(parent)
# GUI
btn_launch = QtWidgets.QPushButton('launch maya')
btn_launch.clicked.connect(self.on_launch)
# Layout
main_layout = QtWidgets.QHBoxLayout(self)
main_layout.addWidget(btn_launch)
self.setLayout(main_layout)
# Root path exe vs ide
if getattr(sys, 'frozen', False):
self.root_path = sys._MEIPASS
else:
self.root_path = os.path.join(os.path.dirname(os.path.realpath(__file__)))
def _set_app_envs(self):
_envs = os.environ.copy()
_envs['MAYA_SCRIPT_PATH'] = os.path.join(self.root_path, 'scripts').replace('\\', '/')
# Python path envs
_python_path_list = [
'C:\\Program Files\\Autodesk\\Maya2020\\Python\\Lib\\site-packages',
'C:\\Program Files\\Autodesk\\Maya2020\\Python\\DLLs',
os.path.join(self.root_path, 'scripts').replace('\\', '/'),
os.path.join(self.root_path, 'internal_source', 'maya_app')
]
# PYTHONPATH exe vs ide
if getattr(sys, 'frozen', False):
_envs['PYTHONPATH'] = os.pathsep.join(_python_path_list)
_envs['PYTHONHOME'] = 'C:\\Program Files\\Autodesk\\Maya2020\\bin'
else:
_envs['PYTHONPATH'] += os.pathsep + os.pathsep.join(_python_path_list)
return _envs
def on_launch(self):
# Maya file path
file_path_abs = '{}/scenes/test.mb'.format(self.root_path).replace('\\', '/')
print(file_path_abs)
app_exe = r'C:/Program Files/Autodesk/Maya2020/bin/maya.exe'
_envs = self._set_app_envs()
if os.path.exists(file_path_abs):
proc = subprocess.Popen(
[app_exe, file_path_abs],
env=_envs,
stdin=subprocess.PIPE,
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
shell=True,
creationflags=subprocess.CREATE_NEW_PROCESS_GROUP
)
if __name__ == "__main__":
app = QtWidgets.QApplication(sys.argv)
window = Widget()
window.resize(400, 400)
window.show()
sys.exit(app.exec_())
bundle.spec
# -*- mode: python ; coding: utf-8 -*-
block_cipher = None
added_files = [
('./scenes', 'scenes'),
('./scripts', 'scripts')
]
a = Analysis(['launch_maya.py'],
pathex=[
'D:/GitStuff/mb-armada/example_files/exe_bundle',
'D:/GitStuff/mb-armada/dependencies/Qt.py',
'D:/GitStuff/mb-armada/venv/Lib/site-packages',
],
binaries=[],
datas=added_files,
hiddenimports=['internal_source', 'internal_source.maya_app'],
hookspath=[],
runtime_hooks=[],
excludes=[],
win_no_prefer_redirects=False,
win_private_assemblies=False,
cipher=block_cipher,
noarchive=False)
pyz = PYZ(a.pure, a.zipped_data,
cipher=block_cipher)
exe = EXE(pyz,
a.scripts,
[],
exclude_binaries=True,
name='bundle',
debug=False,
bootloader_ignore_signals=False,
strip=False,
upx=True,
console=True )
coll = COLLECT(exe,
a.binaries,
a.zipfiles,
a.datas,
strip=False,
upx=True,
upx_exclude=[],
name='bundle')
maya_app.py
import os
import sys
import subprocess
from PySide2 import QtWidgets
class MainWidget(QtWidgets.QWidget):
def __init__(self, parent=None):
super(MainWidget, self).__init__(parent)
# GUI
btn_launch = QtWidgets.QPushButton('say hey')
btn_launch.clicked.connect(self.on_say_hey)
# Layout
main_layout = QtWidgets.QHBoxLayout(self)
main_layout.addWidget(btn_launch)
self.setLayout(main_layout)
print('I should be alive')
def on_say_hey(self):
print('hey')
if __name__ == "__main__":
app = QtWidgets.QApplication(sys.argv)
window = MainWidget()
window.resize(100, 100)
window.show()
sys.exit(app.exec_())
userSetup.py
import os
import sys
import maya.cmds as mc
print('hey')
def tweak_launch(*args):
print('Startup sequence running...')
os.environ['mickey'] = '--------ebae--------'
print(os.environ['mickey'])
root_path = os.getenv('_MMM_ROOT_PATH')
main_app_path = os.path.join(root_path, 'internal_source')
if not root_path in sys.path:
sys.path.append(main_app_path)
from internal_source import maya_app
w = maya_app.MainWidget()
w.show()
print('window should be up')
mc.evalDeferred("tweak_launch()")
At the highest level, this seems like it's really just about path management within your launched maya -- presumably the launcher app knows it's own paths, and the maya you want to fire off needs to be told about their existence.
The most reliable and least magic-prone way to do this is just to have your launcher unbundle any scripts scripts it needs to share with maya proper to a known location -- a hidden directory or even a session-specific temp directory will give you the maximum likelihood that you've got the latest version as your launcher revs.
When launching maya you can pass the paths a few ways -- probably the easiest is just to use site.addsitedir to put them on the path for that session rather than relying on Maya's many other possible search locations. You can launch the maya with the -c mel flag and pass a python command at startup, so you can control this behavior entirely from the launcher and not have to worry about interactions with userSetup.py. Something along these lines would make your stuff available to maya:
# unpack the 'payload' of scripts to share to a
# known location with something like pkgutil.get_data()
# https://docs.python.org/3/library/pkgutil.html
# note the single quotes -- you need them to handle escaping!
py_cmd = "import site; site.addsitedir('{}'); import my_startup_module"
python_to_execute = py_cmd.format(path_to_unpacked_modules)
# again, note annoying escapes
start_cmd = '\"python(\"{}\");\"'.format(python_to_execute)
maya_session = subprocess.Popen(['maya.exe', '-c', start_cmd])
If your exe has unpacked the files to path_to_unpacked_modules, maya will run that mel command, which will call python, add your module dir as a site-packages directory, and then import and run my_startup_module (from that location). This lets your launcher control the startup process and keeps you from having to fiddle separately with userSetup.py -- plus, since it does not touch env vars, you don't have to work to hard to run different sessions side by side.
If the code you want to share is just a bunch of modules (no binary extensions) you can keep your modules in a zip file and add that to the path -- python will be able to find modules inside the zip file automatically. That saves unpack time and also ensure you don't have leftover PYC files from older runs.
There's a lot of useful info in these threads:
https://tech-artists.org/t/python-maya-startup-script/2145/17
https://tech-artists.org/t/deploy-tools-for-your-maya-team/5029/13
Related
I wrote a simple script using Multiprocessing to fetch corresponding records from multiple databases, each containing different info for the same record. The script runs fine on my IDE and cmd, but whenever I tried to compile it with PyInstaller I got instant black screen followed by the sound of tons of error messages.
This is based on Python36, involving psycopg2, fiona and shapely as the files generated are shapefiles. But I compiled other scripts involving these modules before and they work fine.
import psycopg2
import fiona
from shapely.geometry import mapping
from shapely.wkt import loads
import re
import numpy
from multiprocessing import Process
def A(list):
for file in list:
query database A, generate shapefile for product A
def B(list):
for file in list:
query database B, generate shapefile for product B
def C(list):
for file in list:
query database C, generate shapefile for product C
if __name__ == '__main__':
e = Process(target=A, args=(scopes,))
m = Process(target=B, args=(scopes,))
p = Process(target=C, args=(scopes,))
e.start()
m.start()
p.start()
e.join()
print("\nProduct A is done")
m.join()
print("Product B is done")
p.join()
print("Product C is done")
The spec generated during the compiling are like this:
# -*- mode: python -*-
block_cipher = None
a = Analysis(['Scope Process Monitor.py'],
pathex=['C:\\Python36-32\\Lib\\site-packages\\osgeo', 'C:\\Users\\myname\\OneDrive - Company\\Scripts\\Scope Process Monitor'],
binaries=[],
datas=[],
hiddenimports=[],
hookspath=[],
runtime_hooks=[],
excludes=[],
win_no_prefer_redirects=False,
win_private_assemblies=False,
cipher=block_cipher,
noarchive=False)
pyz = PYZ(a.pure, a.zipped_data,
cipher=block_cipher)
exe = EXE(pyz,
a.scripts,
a.binaries,
a.zipfiles,
a.datas,
[],
name='Scope Process Monitor',
debug=False,
bootloader_ignore_signals=False,
strip=False,
upx=True,
runtime_tmpdir=None,
console=True )
I am guessing I need to limit the use of CPU in the script somewhere, but this is actually my first time trying Multiprocessing module, so I do not know how.
this is my first question on stack-overflow!
I am currently writing a Python script (well, actually a few scripts) to manage book collections, that I would now like to freeze and distribute (this is my first 'big' project).
After looking at many options I decided to try with Cx_Freeze.
(I'm using Python 3.6 and Cx_Freeze 5.1.1).
In this project I often use 'subprocess' to move from a script to another.
In the interpreter it works just fine, if I let Cx_Freeze make the build folder using
python setup.py build
it works as well, but when I try to create a distributable file with
python setup.py bdist_msi
after installation it starts and works up to the first call for a subprocess, then nothing more.
Here is setup.py
from cx_Freeze import setup, Executable
import os.path
PYTHON_INSTALL_DIR = os.path.dirname(os.path.dirname(os.__file__))
os.environ['TCL_LIBRARY'] = os.path.join(PYTHON_INSTALL_DIR, 'tcl', 'tcl8.6')
os.environ['TK_LIBRARY'] = os.path.join(PYTHON_INSTALL_DIR, 'tcl', 'tk8.6')
setup(
name = "Libro",
version = "1.0.0",
options = {"build_exe": {
'packages': ["tkinter", "subprocess", ],
'include_files': [os.path.join(PYTHON_INSTALL_DIR, 'DLLs','tk86t.dll'), \
os.path.join(PYTHON_INSTALL_DIR, 'DLLs', 'tcl86t.dll'), \
'logo50x50.gif', 'check_1.txt', 'check_2.txt', 'start.py', \
'createdb.py', *and_a_few_more_files*],
'include_msvcr': True,
}},
executables = [Executable("Libro.py",base="Win32GUI")]
And this is the Libro.py script that becomes the executable.
#This script checks the documents check_1 and check_2 and then launches
# createdb.py or start.py
import subprocess
from tkinter import *
import tkinter.messagebox as box
root= Tk()
root.withdraw()
with open('check_1.txt', 'r') as check_1:
for line in check_1:
line = line.strip()
value_1 = int(line)
with open('check_2.txt', 'r') as check_2:
for line in check_2:
line = line.strip()
value_2 = int(line)
if value_1 == 0 and value_2 == 0:
box.showinfo('Libro 1.0', '''
Welcome to the installation of Libro.
I am now creating the database for your catalogue.
This may take a moment.''')
subprocess.call("createdb.py", shell=True)
else:
subprocess.call("start.py", shell=True)
root.mainloop()
It starts, it looks for check_1 and check_2, shows the tkinter showinfo window and then... that's it.
I would be very grateful for any suggestion!! Thanks :)
You would need to freeze all of your scripts, not just the top level one! (create multiple Executable() entries). And then call subprocess to run the frozen executables. If you don't do that you'll end up requiring Python to be installed on the target machine -- and then why freeze any of it! Of course, it might also be helpful to explain why you need to run your code in a subprocess instead of directly.
In the end it looks like it would be easier and more economic to treat my scripts as modules and then import them when needed.
I tried some simplified operations and they seem to work.
For example:
being main.py
from tkinter import *
from modules import from_file
root = Tk()
root.title('Trial window')
btn_1 = Button(root, text='Read from file', command=from_file)
btn_1.grid(row=1, column=1)
and being modules.py
from tkinter import *
def from_file():
ft = open('text.txt', 'r')
string = ''
for line in ft:
line = line.strip()
string = string+line
ft.close()
root2 = Tk()
result = Label(root2, text=string)
result.grid(row=1, column=1)
root2.mainloop()
the script reads and visualize the content of 'text.txt' in the new windows it opens also after being frozen with cx_freeze.
PS The setup.py I used is
from cx_Freeze import setup, Executable
import os.path
PYTHON_INSTALL_DIR = os.path.dirname(os.path.dirname(os.__file__))
os.environ['TCL_LIBRARY'] = os.path.join(PYTHON_INSTALL_DIR, 'tcl', 'tcl8.6')
os.environ['TK_LIBRARY'] = os.path.join(PYTHON_INSTALL_DIR, 'tcl', 'tk8.6')
setup(
name = "Prova",
version = "1.0.0",
options = {"build_exe": {
'packages': ["tkinter"],
'include_files' : [os.path.join(PYTHON_INSTALL_DIR, 'DLLs', 'tk86t.dll'), \
os.path.join(PYTHON_INSTALL_DIR, 'DLLs', 'tcl86t.dll'), 'text.txt'],
'include_msvcr': True,
}},
executables = [Executable("main.py", base="Win32GUI")]
)
Trying to compile a Python 3.3 application but running into problems after compiling it to win32 exe using cx_freeze on Windows 7.
Example:
Test.py
#!/usr/bin/env python
# -*- coding: utf-8 -*-
import argparse
import tkinter.messagebox
cl_parser = argparse.ArgumentParser(
description='Test app Command Line Parameters')
cl_parser.add_argument('--version', action='version', version='%(prog)s 1.0')
cl_parser.add_argument('-s', '--show', help='show simple message box',
action='store_true')
cl_args = cl_parser.parse_args()
if cl_args.show:
tkinter.messagebox.showinfo(title='Simple Message', message='Hi There')
Cx_Freeze Script
import sys
from cx_Freeze import setup, Executable
import shutil
#Remove Build/Dist Folder Before Compile
args = None
print('Removing Old Folders...')
try:
for args in sys.argv:
if args.find('build_exe') != -1:
shutil.rmtree('build', ignore_errors=True)
elif args.find('bdist_msi') != -1:
shutil.rmtree('dist', ignore_errors=True)
except:
pass
finally:
del args
print('Folders Removed')
base = None
if sys.platform == 'win32':
base = 'Win32GUI'
options = {
'build_exe': {
'includes': 'atexit',
'include_msvcr': True
}
}
executables = [
Executable('Test.py', base=base, targetName='Test App.exe')
]
setup(name='Test App',
version='1.0',
description='Test App',
options=options,
executables=executables)
After the successful compile I try a few command line tests on the exe:
Test App.exe -s (This successfully shows the messagebox)
Test App.exe -h or TestApp.exe --version (Fails with AttributeError: 'NoneType' Object has no attribute 'write')
Is there a way that I can make it so that this will work? I am trying to create a PyQt4 application and don't want to have to compile this as a console application.
Your feedback would be appreciated
P.S. Yes I know i've included an example with tkinter but this is for demo purposes only.
I'm trying to show a QFileDialog using the following piece of code:
import os, sys
from PyQt4.QtGui import *
class MainWindow(QWidget):
def __init__(self):
QWidget.__init__(self)
self._button = QPushButton('Test button')
self._button.clicked.connect(self._onButtonClicked)
self._layout = QHBoxLayout()
self._layout.addWidget(self._button)
self.setLayout(self._layout)
def _onButtonClicked(self):
self._dialog = QFileDialog(self, 'Select directory')
self._dialog.setDirectory(os.getenv('HOME'))
self._dialog.setFileMode(QFileDialog.Directory)
self._dialog.directoryEntered.connect(self._onDirEntered)
self._dialog.exec_()
def _onDirEntered(self, directory):
print("Entered directory: %s" % (directory))
if __name__ == "__main__":
app = QApplication(sys.argv)
mw = MainWindow()
mw.show()
app.exec_()
Two problems here:
The directoryEntered signal is never emitted, at least I don't get any output from the script (except for some KDE warnings about Samba support, etc.); actually no signal from the QFileDialog class I tried to connect to gets emitted in the example. What am I doing wrong?
In this example I set the starting directory to $HOME, but the dialog will start in /home instead and have my home directory selected in the listing instead of starting directly in my home directory. Can I change this behaviour somehow?
I'm using Python 3.4.0 with PyQt 4.10.4-2.
Both problems do not occur when using a non-native dialog as suggested by #ekhumoro.
Update:
I suspect this is an error in cxFreeze as I understand this should
go automatically.
end update
Update:
I missed an error given by cxFreeze:
Missing modules:
? Test.MyClass imported from main__main__
end update
I'm not sure what the proper term is for modules within project unlike sys or PyQt, so I'm going with internal project modules.
I have some example code below where I recieve the error "ImportError: cannot import name MyClass." and I would love to know how to get cxFreeze to compile that 'Test.py' module.
Here's my main code:
Main.py
import sys
from PyQt5.QtWidgets import QApplication, QMainWindow
#from guiObjects.MainWindow import MainWindow
from Test import MyClass
if __name__ == "__main__":
# Initializing the main window
app = QApplication(sys.argv)
widget = QMainWindow()
#mainWindow = MainWindow(widget)
test = MyClass()
widget.show()
sys.exit(app.exec_())
Test.py
class MyClass(object):
def __init__(self):
pass
__init.py__
'''empty'''
Setup.py
import sys
from cx_Freeze import setup, Executable
path_platforms = ( "C:\Python33\Lib\site-packages\PyQt5\plugins\platforms\qwindows.dll", "platforms\qwindows.dll" )
includes = ["re","sip","atexit","PyQt5.QtCore","PyQt5.QtGui"]
includefiles = [path_platforms]
excludes = [
'_gtkagg', '_tkagg', 'bsddb', 'curses', 'email', 'pywin.debugger',
'pywin.debugger.dbgcon', 'pywin.dialogs', 'tcl',
'Tkconstants', 'Tkinter'
]
packages = ["os"]
path = []
# Dependencies are automatically detected, but it might need fine tuning.
build_exe_options = {
"includes": includes,
"include_files": includefiles,
"excludes": excludes,
"packages": packages,
"path": path
}
# GUI applications require a different base on Windows (the default is for a
# console application).
base = None
exe = None
if sys.platform == "win32":
exe = Executable(
script="../Main.py",
initScript = None,
base="Win32GUI",
targetDir = r"dist",
targetName="Main.exe",
compress = True,
copyDependentFiles = True,
appendScriptToExe = False,
appendScriptToLibrary = False,
icon = None
)
setup(
name = "Main",
version = "0.1",
author = 'me',
description = "My GUI application!",
options = {"build_exe": build_exe_options},
executables = [exe]
)
This problem happens when you run setup.py in a subfolder of where Main.py is located.
I now placed my setup.py in the same folder as Main.py. and changed my .bat file to python ../setup.py build install.
This seems to be a bug in cx_Freeze as it works fine for Python 2.7, but not Python 3.3.
Your test.py is wrong, you can't leave functions empty, try
class MyClass(object):
def __init__(self):
pass
and in setup.py mabye add "Test" into "includes"
includes = ["re","sip","atexit","PyQt5.QtCore","PyQt5.QtGui", "Test"]