Pyzo

General Information

Motivation

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.

Screenshot of Pyzo running the FreeCAD Python Interpreter

Pyzo

Versions

Unless otherwise noted, this page assumes the following software versions:

Set-Up and Configuration

Preparing a FreeCAD AppImage in Linux

#!/bin/bash
HERE="$(dirname "$(readlink -f "${0}")")"
${HERE}/AppRun python "$@"

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.

Installing 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

Shell Configuration

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:

name
freecad (or something else)
exe [for Windows]
C:\Program Files\FreeCAD 0.21\bin\python.exe
exe [for Linux]
/home/.../FreeCAD/python_freecad.sh
exe [for macOS]
/Applications/FreeCAD.app/Contents/Resources/bin/python
Or, in case of this error: Fatal Python error: take_gil: PyMUTEX(gil->mutex) failed, try:
/Applications/FreeCAD.app/Contents/Resources/bin/freecadcmd
gui
PySide2
pythonPath [for Windows and Linux]
[leave empty]
pythonPath [for macOS]
/Applications/FreeCAD.app/Contents/Resources/lib
startupScript
select radio button "Code to run at startup"
enter the following code in the text field, replacing everything that was there before:
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()
environ
enter the following environment variables:
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

General Pyzo Usage

See https://pyzo.org/guide.html and run the Pyzo Wizard (Menu: Help -> Pyzo Wizard) to get a short introduction.

Noteworthy information:

Example Work Flows

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).

Example for manipulating an object in a shape generation macro

  1. Start Pyzo with FreeCAD, switch to the Part workbench and create a new file.
  2. Open file "Mod/Part/BasicShapes/Shapes.py" (inside the FreeCAD folder) in Pyzo.
  3. Set a breakpoint at line 37 (inner_cylinder = Part.makeCylinder(innerRadius, height)).
  4. Apply the breakpoints, for example by pressing Return in the shell in Pyzo.
  5. In FreeCAD, run "Part -> Primitives -> Create tube".
  6. Function "makeTube" is now interrupted via the breakpoint.
  7. Execute the command outer_cylinder.rotate((0, 0, 0), (0, 1, 0), 45) in Pyzo's shell.
  8. Continue execution, e.g. via "Shell -> Debug continue"
  9. Now you have a cylinder with a misaligned hole.

Example for skipping code in a shape generation macro

  1. Start Pyzo with FreeCAD, switch to the Part workbench and create a new file.
  2. Open file "Mod/Part/BasicShapes/Shapes.py" (inside the FreeCAD folder) in Pyzo.
  3. Set a breakpoint at line 37 (inner_cylinder = Part.makeCylinder(innerRadius, height)).
  4. Apply the breakpoints, for example by pressing Return in the shell in Pyzo.
  5. In FreeCAD, run "Part -> Primitives -> Create tube".
  6. Function "makeTube" is now interrupted via the breakpoint.
  7. In Pyzo, set the text cursor to line 39 (return shape).
  8. Run "Shell -> Debug jump" or click the new icon in the shell's toolbar.
  9. Continue execution, e.g. via "Shell -> Debug continue"
  10. Now you have a cylinder instead of a pipe because you skipped creating the inner cylinder and cutting it.

Example for using breakpoints in callbacks

This is a step-by-step demonstration of how breakpoints can be used to debug callbacks in FeaturePython objects.

  1. Copy the full example code from https://wiki.freecad.org/Create_a_FeaturePython_object_part_II#Complete_code and save it to a file (to have a module), e.g. /tmp/mymodules/mytest.py
  2. Start Pyzo with a FreeCAD shell and wait till the FreeCAD GUI is fully loaded.
  3. Run the following commands in the Pyzo shell (or in the Python console panel in FreeCAD's GUI):
    import sys
    sys.path.append('/tmp/mymodules')
    
    import mytest
    
    doc = App.newDocument()
    mytest.create('abcde')
    
  4. Open file "/tmp/mymodules/mytest.py" in Pyzo (if not already open).
  5. Set a breakpoint at the line obj.Shape = ... in method execute(self, obj) of class Box.
  6. Breakpoints are updated in Pyzo when giving a command to execute code (or continuing execution when the debugger is active). Therefore run the dummy code pass in Pyzo's shell or just press Return there so that the breakpoint will be applied.
  7. Switch to the FreeCAD GUI, select object "abcde" and change the value of property "Height", for example from 10 to 20 mm.
  8. Now, the breakpoint is triggered. Switch to Pyzo and inspect the value of obj.Height by executing 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.

Example for monkey patching FeaturePython objects

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.

Advanced Topics

Creating a Custom Tool

Custom tool example "MyRunner" in Pyzo

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

Debugging Start-Up of FreeCAD Modules

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.

Pyzo customizations developed by FreeCAD users

Workspace2 by TheMarkster et al.

https://forum.freecad.org/viewtopic.php?p=720709#p720709

A modified "workspace" tool that adds filters and a watchlist.

WorkspaceWatch by heda

https://forum.freecad.org/viewtopic.php?p=774260#p774260

A modified "workspace" tool that adds an extra treeview widget for displaying "watched" variables.

GitBashTool by TheMarkster

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.

Known Limitations and Issues

General issues caused by importing FreeCAD as a module in Python

Wrong scaling for the FreeCAD GUI on multiple screens with different resolutions

problem description
https://forum.freecad.org/viewtopic.php?p=774781#p774781
solution
https://forum.freecad.org/viewtopic.php?p=774803#p774803
https://forum.freecad.org/viewtopic.php?p=774972#p774972

Fontconfig related crash when switching to the Draft workbench because of third-party software "Graphviz"

problem description
https://forum.freecad.org/viewtopic.php?p=776449#p776449
solution
https://forum.freecad.org/viewtopic.php?p=776751#p776751

Feedback

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.

Miscellaneous Links