Pyzo is a free and open-source Python IDE focused on interactivity and introspection, which makes it well-suited for engineering and scientific applications. This page describes how to use it in conjunction with FreeCAD.
Unless otherwise noted, this page assumes the following software versions:
$ chmod +x ./FreeCAD_0.21.2-Linux-x86_64.AppImage
$ ./FreeCAD_0.21.2-Linux-x86_64.AppImage --appimage-extract
$ mv ./squashfs-root FreeCAD
export FREECAD_USER_HOME=$HOME
]#!/bin/bash
HERE="$(dirname "$(readlink -f "${0}")")"
${HERE}/AppRun python "$@"
$ ./FreeCAD/AppRun
and close it again so that the user config files are created properly.Note that, on some Linux systems, the FreeCAD 0.21.2 AppImage reports an error "MESA-LOADER: failed to open ..." when being started. In that case execute $ rm ./FreeCAD/usr/lib/libdrm*
after extracting and renaming the AppImage, as suggested here.
python3 /path/to/pyzo-main/pyzolauncher.py
. To use a specific Qt API instead of the automatically detected one, set environment variable "QT_API" before. Possible values are: pyside2, pyside6, pyqt5, pyqt6.python3 -m pip install pyzo
Linux users please run Pyzo from source because there could be library incompatibilities with the provided binary version of Pyzo, resulting in a crash when opening the file dialog.
See also the chapter "Installation" in the Pyzo ReadMe: https://github.com/pyzo/pyzo#installation
Start Pyzo and enter the shell configuration dialog via the Menu:
Shell -> Edit shell configurations...
A dialog window will appear.
If there was no old shell configuration found, then Pyzo will create a single tab named "Python" with an empty "exe" textbox. Fill out this tab with the values provided below.
Otherwise, if there is no tab with an empty "exe" textbox, press the button "Add config" on the top right corner, and then fill out the tab:
from PySide2 import QtCore, QtWidgets
QtCore.QCoreApplication.setAttribute(QtCore.Qt.AA_ShareOpenGLContexts, True)
# optional switches:
# QtWidgets.QApplication.setAttribute(QtCore.Qt.AA_EnableHighDpiScaling, True)
# QtCore.QCoreApplication.setAttribute(QtCore.Qt.AA_UseHighDpiPixmaps, True)
# AFTER_GUI - code below runs after integrating the GUI
import os, sys
if sys.platform == 'linux': # ... and using the (extracted) AppImage
sys.path.append(os.environ['PATH_TO_FREECAD_LIBDIR'])
# some older Linux systems require the following line:
# os.environ['FREECAD_USER_HOME'] = os.environ['HOME']
import FreeCAD as App
import FreeCADGui as Gui
App.ParamGet('User parameter:BaseApp/Preferences/OutputWindow').SetBool('RedirectPythonOutput', False)
App.ParamGet('User parameter:BaseApp/Preferences/OutputWindow').SetBool('RedirectPythonErrors', False)
Gui.showMainWindow()
PYZO_PROCESS_EVENTS_WHILE_DEBUGGING=1
LC_NUMERIC=C
Finally, press button "Done" in the shell configuration dialog. Run a new "freecad" shell via Pyzo's "Shell" menu. This will start the FreeCAD Gui and a will open a FreeCAD Python shell in Pyzo.
On Linux, when not using an (extracted) AppImage, change sys.path.append(os.environ['PATH_TO_FREECAD_LIBDIR'])
in the text field "startupScript" to e.g. sys.path.append('/builds/freecad-source/build/lib')
, specifying the directory to the library files "FreeCAD.so" and "FreeCADGui.so". This library directory path could also be added to the field "pythonPath" in the Pyzo shell configuration dialog instead.
The environment variable entry PYZO_PROCESS_EVENTS_WHILE_DEBUGGING=1
will tell Pyzo to periodically update FreeCAD's Qt-GUI while FreeCAD is stopped during a breakpoint.
Do not remove the # AFTER_GUI
comment in the newly entered startup code -- this is a separator used by Pyzo to split the code into two blocks.
If you have an older Mac and pyzo fails to launch (e.g. pyzo 4.13.2 on OS-X 10.15), a possible work-around is to launch a patched pyzo 4.12.7 from the Terminal. See FreeCAD forum message
See https://pyzo.org/guide.html and run the Pyzo Wizard (Menu: Help -> Pyzo Wizard) to get a short introduction.
Noteworthy information:
pyzo.config.shellConfigs2.insert(0, pyzo.config.shellConfigs2[1].copy())
.__file__
variable defined in the interactive mode, but as a workaround you can run import inspect; __file__ = inspect.getfile(inspect.currentframe())
to get it.A normal workflow to automate FreeCAD operations is similar to using just FreeCAD. The shell in Pyzo has access to the same Python interpreter as the "Python console" panel in the FreeCAD GUI. Both can be used in a mixed fashion, whatever is more convenient in the situation.
Pyzo brings very nice features to this workflow, just to list some of them:
Have a look here and here to see this in action (with an older version where the FreeCAD start was not yet included in the Pyzo shell configuration).
inner_cylinder = Part.makeCylinder(innerRadius, height)
).outer_cylinder.rotate((0, 0, 0), (0, 1, 0), 45)
in Pyzo's shell.inner_cylinder = Part.makeCylinder(innerRadius, height)
).return shape
).This is a step-by-step demonstration of how breakpoints can be used to debug callbacks in FeaturePython objects.
import sys
sys.path.append('/tmp/mymodules')
import mytest
doc = App.newDocument()
mytest.create('abcde')
obj.Shape = ...
in method execute(self, obj)
of class Box
.pass
in Pyzo's shell or just press Return there so that the breakpoint will be applied.obj.Height
in the shell. Change the value by executing obj.Height = 400
and continue execution via "Shell -> Debug continue". Switch back to the FreeCAD GUI -- the box shape now has a height of 400 mm.Normally, the code for FeaturePython objects is part of a module. Once FreeCAD objects are created, changes to the code in the module will only take effect after closing the FCStd-file, reloading the module, and re-opening the FCStd file.
To make quick iteration cycles during development of FeaturePython code, a monkey patching strategies can be used, as demonstrated with the following example.
First we create the module with the FeaturePython object's code. Create a new python script with the following code and save it into your user macros directory as "mymod.py".
import FreeCAD as App
def create(obj_name):
obj = App.ActiveDocument.addObject('Part::FeaturePython', obj_name)
MyBox(obj)
App.ActiveDocument.recompute()
return obj
class MyBox():
def __init__(self, obj):
self.Type = self.__class__.__name__
obj.Proxy = self
def execute(self, obj):
# called on document recompute
self.my_computations(obj)
## start of code for monkeypatching
def my_computations(self, obj):
print(obj.Label, 'aaa')
if '__module__' not in dir():
def _monkeypatch_method(classname, method):
import inspect, os, sys
__this_file__ = os.path.abspath(inspect.getfile(inspect.currentframe()))
for mod in sys.modules.values():
if __this_file__.startswith(getattr(mod, '__file__', None) or '_'):
setattr(getattr(mod, classname), method.__name__, method)
_monkeypatch_method('MyBox', my_computations)
## end of code
We assume that, at this point, your already have started FreeCAD via Pyzo. Execute the following code (either in Pyzo's FreeCAD-shell or in FreeCAD's Python console panel):
import mymod
doc = App.newDocument()
mymod.create('obj1')
mymod.create('obj2')
doc.recompute()
Now, everytime you recompute one of the two created FeaturePython objects, the object's label and "aaa" will be printed in Pyzo's shell.
We want to quickly change the behavior of the FeaturePython object so that it prints "bbb" instead of "aaa". Open the module in Pyzo's editor (if not already opened) and change the string literal "aaa" to "bbb". To apply these changes, just execute that cell (which is defined by the two "##" comment lines) in Pyzo by pressing Ctrl+Return. Recompute the objects, and we have the desired result. Editing the method and executing the cell can now be repeated as often as desired. Debugging also works normally, e.g. using breakpoints. If you want set breakpoints below the code cell, there might be a line offset because the rest of the class definition is not updated. A nice aspect of this strategy is that the code is directly modified in the real module, so when just saving the module, restarting FreeCAD and re-opening the FCStd file, you already have the newest version without any monkey patching.
Pyzo includes a Plug-In concept to extend its features without patching its source code.
See the example code in the Pyzo repository:
https://github.com/pyzo/pyzo/blob/main/pyzo/resources/code_examples/myRunner.py
To debug the init script of a FreeCAD Mod, e.g. files such as Mod/Draft/Init.py or Mod/Draft/InitGui.py using breakpoints, we need to set a breakpoint in the code file. These init script files are not directly executed.
During startup, FreeCAD runs the Python scripts src/App/FreeCADInit.py and src/Gui/FreeCADGuiInit.py, and each of these reads the corresponding init scripts (App and Gui) of the Mod and executes them as a string with the code contents:
with open(file=InstallFile, encoding="utf-8") as f:
exec(f.read())
The interpreter has no more information about the filepath of the executed code. To include the filepath, we need to patch the lines above. FreeCAD version 0.22 or newer already contain this patch.
This is not all. The scripts src/App/FreeCADInit.py and src/Gui/FreeCADGuiInit.py are not individual files anymore but resource files in libraries in the distributed binary versions of FreeCAD. Therefore we will extract them into files and replace the data in the libraries with a short Python script that will call our new out-sourced scripts. So we could also open these new files and set breakpoints there.
The following code will extract the scripts from the libraries, patch them and place the caller code back in the libraries:
import os
import sys
import shutil
lib_dirpath = '/home/.../FreeCAD/usr/lib' # example for linux os
# lib_dirpath = r'C:\ProgramData\FreeCAD\bin' # example for windows os
restore_original_libs = False
# restore_original_libs = True
lib_dirpath = os.path.abspath(lib_dirpath)
for appgui in ['App', 'Gui']:
if sys.platform == 'linux':
fp = os.path.join(lib_dirpath, 'libFreeCAD{}.so'.format(appgui))
elif sys.platform == 'win32':
fp = os.path.join(lib_dirpath, 'FreeCAD{}.dll'.format(appgui))
else: raise NotImplementedError()
fp_backup = fp + '.orig'
if restore_original_libs:
shutil.copy(fp_backup, fp)
continue
if not os.path.isfile(fp_backup):
shutil.copy(fp, fp_backup)
with open(fp_backup, 'rb') as fd:
dd = fd.read()
if appgui == 'App':
needle = b'\n# FreeCAD init module\n'
fp_py = fp + '.FreeCADInit.py'
else:
needle = b'\n# FreeCAD gui init module\n'
fp_py = fp + '.FreeCADGuiInit.py'
i_needle = dd.index(needle)
assert dd.find(needle, i_needle + 1) == -1
i_start = i_needle - dd[:i_needle][::-1].index(b'\x00')
i_end = dd.index(b'\x00', i_needle)
dd_code = dd[i_start:i_end]
fp_py_orig = fp_py + '.orig'
with open(fp_py_orig, 'wb') as fd:
fd.write(dd_code)
loader_code = '\n'.join([
'fp = ' + repr(fp_py),
'with open(fp, "rt", encoding="utf-8") as fd: source = fd.read()',
'exec(compile(source, fp, "exec"))',
]).format(repr(fp_py))
dd_code_new = loader_code.encode('utf-8')
assert len(dd_code_new) <= len(dd_code)
dd_code_new += b'\x00' * (len(dd_code) - len(dd_code_new))
dd_new = dd[:i_start] + dd_code_new + dd[i_end:]
assert len(dd_new) == len(dd)
with open(fp, 'wb') as fd:
fd.write(dd_new)
tt_code = dd_code.decode('utf-8')
needle = """
with open(file=InstallFile, encoding="utf-8") as f:
exec(f.read())
"""
needle_patched = """
with open(InstallFile, 'rt', encoding='utf-8') as f:
exec(compile(f.read(), InstallFile, 'exec'))
"""
if needle in tt_code:
# older FreeCAD versions < 0.22 need to be patched
tt_code = tt_code.replace(needle, needle_patched)
else:
assert needle_patched in tt_code
with open(fp_py, 'wt', encoding='utf-8') as fd:
fd.write(tt_code)
We can now, for example, open Mod/Draft/Init.py in Pyzo, set a breakpoint there in line 26 and start the FreeCAD shell in Pyzo. Execution will be interrupted at the breakpoint. We can switch the stack frames, view and manipulate variables and step/continue the code execution.
https://forum.freecad.org/viewtopic.php?p=720709#p720709
A modified "workspace" tool that adds filters and a watchlist.
https://forum.freecad.org/viewtopic.php?p=774260#p774260
A modified "workspace" tool that adds an extra treeview widget for displaying "watched" variables.
https://forum.freecad.org/viewtopic.php?p=774810#p774810
Displays a push button to open a git bash shell in the path of the file in the current editor.
To ask questions about this topic, share ideas, give input, point out mistakes, etc, please write a message to the initial topic in the FreeCAD forum or create a new one.