fix: preserve caller globals in exec() for module Init.py/InitGui.py loading #240

Merged
forbes merged 1 commits from fix/exec-globals-regression into main 2026-02-15 10:22:11 +00:00
2 changed files with 226 additions and 133 deletions

View File

@@ -1,28 +1,28 @@
# SPDX-License-Identifier: LGPL-2.1-or-later
#***************************************************************************
#* Copyright (c) 2001,2002 Jürgen Riegel <juergen.riegel@web.de> *
#* Copyright (c) 2025 Frank Martínez <mnesarco at gmail dot com> *
#* *
#* This file is part of the FreeCAD CAx development system. *
#* *
#* This program is free software you can redistribute it and/or modify *
#* it under the terms of the GNU Lesser General Public License (LGPL) *
#* as published by the Free Software Foundation either version 2 of *
#* the License, or (at your option) any later version. *
#* for detail see the LICENCE text file. *
#* *
#* FreeCAD is distributed in the hope that it will be useful, *
#* but WITHOUT ANY WARRANTY without even the implied warranty of *
#* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
#* GNU Lesser General Public License for more details. *
#* *
#* You should have received a copy of the GNU Library General Public *
#* License along with FreeCAD if not, write to the Free Software *
#* Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 *
#* USA *
#* *
#***************************************************************************/
# ***************************************************************************
# * Copyright (c) 2001,2002 Jürgen Riegel <juergen.riegel@web.de> *
# * Copyright (c) 2025 Frank Martínez <mnesarco at gmail dot com> *
# * *
# * This file is part of the FreeCAD CAx development system. *
# * *
# * This program is free software you can redistribute it and/or modify *
# * it under the terms of the GNU Lesser General Public License (LGPL) *
# * as published by the Free Software Foundation either version 2 of *
# * the License, or (at your option) any later version. *
# * for detail see the LICENCE text file. *
# * *
# * FreeCAD is distributed in the hope that it will be useful, *
# * but WITHOUT ANY WARRANTY without even the implied warranty of *
# * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
# * GNU Lesser General Public License for more details. *
# * *
# * You should have received a copy of the GNU Library General Public *
# * License along with FreeCAD if not, write to the Free Software *
# * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 *
# * USA *
# * *
# ***************************************************************************/
# FreeCAD init module - App
#
@@ -47,37 +47,43 @@ App.Console.PrintLog("░░░░█░░█░█░░█░░░█░░
App.Console.PrintLog("░░░▀▀▀░▀░▀░▀▀▀░░▀░░░░▀░▀░▀░░░▀░░░░\n")
try:
import sys
import os
import traceback
import inspect
from enum import IntEnum # Leak to globals (backwards compat)
from datetime import datetime # Leak to globals (backwards compat)
from pathlib import Path # Removed manually
import dataclasses
import collections
import collections.abc as coll_abc
import platform
import types
import importlib.resources as resources
import importlib
import dataclasses
import functools
import re
import importlib
import importlib.resources as resources
import inspect
import os
import pkgutil
import platform
import re
import sys
import traceback
import types
from datetime import datetime # Leak to globals (backwards compat)
from enum import IntEnum # Leak to globals (backwards compat)
from pathlib import Path # Removed manually
except ImportError:
App.Console.PrintError("\n\nSeems the python standard libs are not installed, bailing out!\n\n")
App.Console.PrintError(
"\n\nSeems the python standard libs are not installed, bailing out!\n\n"
)
raise
# ┌────────────────────────────────────────────────┐
# │ Logging Frameworks │
# └────────────────────────────────────────────────┘
def __logger(fn):
__logger.sep = "\n"
def wrapper(text: object, *, sep: str | None = None) -> None:
fn(f"{text!s}{__logger.sep if sep is None else sep}")
return wrapper
Log = __logger(App.Console.PrintLog)
Msg = __logger(App.Console.PrintMessage)
Err = __logger(App.Console.PrintError)
@@ -129,18 +135,18 @@ class FCADLogger:
"""
_levels = {
'Error': 0,
'error': 0,
'Warning': 1,
'warn': 1,
'Message': 2,
'msg': 2,
'info': 2,
'Log': 3,
'log': 3,
'debug': 3,
'Trace': 4,
'trace': 4,
"Error": 0,
"error": 0,
"Warning": 1,
"warn": 1,
"Message": 2,
"msg": 2,
"info": 2,
"Log": 3,
"log": 3,
"debug": 3,
"Trace": 4,
"trace": 4,
}
_printer = (
@@ -148,16 +154,16 @@ class FCADLogger:
App.Console.PrintWarning,
App.Console.PrintMessage,
App.Console.PrintLog,
App.Console.PrintLog
App.Console.PrintLog,
)
_defaults = (
('printTag', True),
('noUpdateUI', True),
('timing', True),
('lineno', True),
('parent', None),
('title', 'FreeCAD'),
("printTag", True),
("noUpdateUI", True),
("timing", True),
("lineno", True),
("parent", None),
("title", "FreeCAD"),
)
printTag: bool
@@ -239,7 +245,7 @@ class FCADLogger:
def log_fn(self, msg: str, *args, **kwargs) -> None:
if self._isEnabledFor(level):
frame = kwargs.pop('frame', 0) + 1
frame = kwargs.pop("frame", 0) + 1
self._log(level, msg, frame, args, kwargs)
log_fn.__doc__ = docstring
@@ -279,29 +285,31 @@ class FCADLogger:
else:
msg = msg.format(*args, **kwargs)
prefix = ''
prefix = ""
if self.timing:
now = datetime.now()
prefix += '{} '.format((now-self.laststamp).total_seconds())
prefix += "{} ".format((now - self.laststamp).total_seconds())
self.laststamp = now
if self.printTag:
prefix += '<{}> '.format(self.tag)
prefix += "<{}> ".format(self.tag)
if self.lineno:
try:
frame = sys._getframe(frame+1)
prefix += '{}({}): '.format(os.path.basename(
frame.f_code.co_filename),frame.f_lineno)
frame = sys._getframe(frame + 1)
prefix += "{}({}): ".format(
os.path.basename(frame.f_code.co_filename), frame.f_lineno
)
except Exception:
frame = inspect.stack()[frame+1]
prefix += '{}({}): '.format(os.path.basename(frame[1]),frame[2])
frame = inspect.stack()[frame + 1]
prefix += "{}({}): ".format(os.path.basename(frame[1]), frame[2])
self.__class__._printer[level]('{}{}\n'.format(prefix,msg))
self.__class__._printer[level]("{}{}\n".format(prefix, msg))
if not self.noUpdateUI and App.GuiUp:
import FreeCADGui
try:
FreeCADGui.updateGui()
except Exception:
@@ -380,7 +388,9 @@ class FCADLogger:
except Exception as e:
self.error(f"{msg}\n{traceback.format_exc()}", frame=1)
if App.GuiUp:
import FreeCADGui, PySide
import FreeCADGui
import PySide
PySide.QtGui.QMessageBox.critical(
FreeCADGui.getMainWindow(),
self.title,
@@ -631,6 +641,7 @@ App.Units.YieldStrength = App.Units.Unit(-1,1,-2)
App.Units.YoungsModulus = App.Units.Unit(-1,1,-2)
# fmt: on
# The values must match with that of the
# C++ enum class UnitSystem
class Scheme(IntEnum):
@@ -645,15 +656,19 @@ class Scheme(IntEnum):
FEM = 8
MeterDecimal = 9
App.Units.Scheme = Scheme
class NumberFormat(IntEnum):
Default = 0
Fixed = 1
Scientific = 2
App.Units.NumberFormat = NumberFormat
class ScaleType(IntEnum):
Other = -1
NoScaling = 0
@@ -661,8 +676,10 @@ class ScaleType(IntEnum):
NonUniformLeft = 2
Uniform = 3
App.ScaleType = ScaleType
class PropertyType(IntEnum):
Prop_None = 0
Prop_ReadOnly = 1
@@ -672,8 +689,10 @@ class PropertyType(IntEnum):
Prop_NoRecompute = 16
Prop_NoPersist = 32
App.PropertyType = PropertyType
class ReturnType(IntEnum):
PyObject = 0
DocObject = 1
@@ -683,6 +702,7 @@ class ReturnType(IntEnum):
LinkAndPlacement = 5
LinkAndMatrix = 6
App.ReturnType = ReturnType
@@ -690,6 +710,7 @@ App.ReturnType = ReturnType
# │ Init Framework │
# └────────────────────────────────────────────────┘
class Transient:
"""
Mark the symbol for removal from global scope on cleanup.
@@ -705,8 +726,12 @@ class Transient:
def cleanup(cls) -> None:
# Remove imports
# os: kept for backwards compat
keep = set(("__builtins__", "FreeCAD", "App", "os", "sys", "traceback", "inspect"))
names = [name for name, ref in globals().items() if isinstance(ref, types.ModuleType)]
keep = set(
("__builtins__", "FreeCAD", "App", "os", "sys", "traceback", "inspect")
)
names = [
name for name, ref in globals().items() if isinstance(ref, types.ModuleType)
]
for name in names:
if name not in keep:
del globals()[name]
@@ -726,6 +751,7 @@ def call_in_place(fn):
fn()
return fn
@transient
class utils:
HLine = "-" * 80
@@ -755,6 +781,7 @@ class utils:
try:
import readline
import rlcompleter # noqa: F401, import required
readline.parse_and_bind("tab: complete")
except ImportError:
# Note: As there is no readline on Windows,
@@ -770,6 +797,7 @@ class PathPriority(IntEnum):
OverrideLast = 3
OverrideFirst = 4
@transient
@dataclasses.dataclass
class PathSet:
@@ -787,10 +815,16 @@ class PathSet:
"""
source: list["Path | PathSet"] = dataclasses.field(default_factory=list)
override: collections.deque["Path | PathSet"] = dataclasses.field(default_factory=collections.deque)
fallback: collections.deque["Path | PathSet"] = dataclasses.field(default_factory=collections.deque)
override: collections.deque["Path | PathSet"] = dataclasses.field(
default_factory=collections.deque
)
fallback: collections.deque["Path | PathSet"] = dataclasses.field(
default_factory=collections.deque
)
def add(self, item: "Path | PathSet", priority: PathPriority = PathPriority.OverrideLast) -> None:
def add(
self, item: "Path | PathSet", priority: PathPriority = PathPriority.OverrideLast
) -> None:
"""Add item into the corresponding priority slot."""
if isinstance(item, Path):
item = item.resolve()
@@ -828,6 +862,7 @@ class PathSet:
"""
return list(dict.fromkeys(self.iter()))
@transient
class SearchPaths:
"""
@@ -843,7 +878,9 @@ class SearchPaths:
dll_path: PathSet
def __init__(self):
self.env_path = PathSet([Path(p) for p in os.environ.get("PATH", "").split(os.pathsep)])
self.env_path = PathSet(
[Path(p) for p in os.environ.get("PATH", "").split(os.pathsep)]
)
self.sys_path = PathSet(sys.path)
self.dll_path = PathSet()
@@ -853,7 +890,7 @@ class SearchPaths:
*,
env_path: PathPriority = PathPriority.OverrideLast,
sys_path: PathPriority = PathPriority.OverrideFirst,
dll_path: PathPriority = PathPriority.OverrideLast
dll_path: PathPriority = PathPriority.OverrideLast,
) -> None:
"""
Add item to required namespaces with the specified priority.
@@ -866,7 +903,9 @@ class SearchPaths:
def commit(self) -> None:
"""Apply changes to underlying namespaces and priorities."""
os.environ["PATH"] = os.pathsep.join(str(path) for path in self.env_path.build())
os.environ["PATH"] = os.pathsep.join(
str(path) for path in self.env_path.build()
)
sys.path = [str(path) for path in self.sys_path.build()]
if win32 := WindowsPlatform():
@@ -880,8 +919,10 @@ class SearchPaths:
class Config:
AdditionalModulePaths = utils.str_to_paths(App.ConfigGet("AdditionalModulePaths"))
AdditionalMacroPaths = utils.str_to_paths(App.ConfigGet("AdditionalMacroPaths"))
RunMode: str = App.ConfigGet('RunMode')
DisabledAddons: set[str] = set(mod for mod in App.ConfigGet("DisabledAddons").split(";") if mod)
RunMode: str = App.ConfigGet("RunMode")
DisabledAddons: set[str] = set(
mod for mod in App.ConfigGet("DisabledAddons").split(";") if mod
)
@transient
@@ -891,7 +932,7 @@ class WindowsPlatform:
"""
initialized = False
enabled = platform.system() == 'Windows' and hasattr(os, "add_dll_directory")
enabled = platform.system() == "Windows" and hasattr(os, "add_dll_directory")
def __init__(self) -> None:
if not WindowsPlatform.enabled or WindowsPlatform.initialized:
@@ -1038,7 +1079,9 @@ class ExtMod(Mod):
def check_disabled(self) -> bool:
with resources.as_file(resources.files(self.name)) as base:
return (base / self.ADDON_DISABLED).exists() or (base.parent.parent / self.ADDON_DISABLED).exists()
return (base / self.ADDON_DISABLED).exists() or (
base.parent.parent / self.ADDON_DISABLED
).exists()
def process_metadata(self, _search_paths: SearchPaths) -> None:
meta = self.metadata
@@ -1047,14 +1090,16 @@ class ExtMod(Mod):
if not self.supports_freecad_version():
self.state = ModState.Unsupported
Msg(f"NOTICE: {self.name} does not support this version of FreeCAD, so is being skipped")
Msg(
f"NOTICE: {self.name} does not support this version of FreeCAD, so is being skipped"
)
def _init_error(self, ex: Exception, error_msg: str) -> None:
Err(f'During initialization the error "{ex!s}" occurred in {self.name}')
Err(utils.HLine)
Err(error_msg)
Err(utils.HLine)
Log(f'Init: Initializing {self.name}... failed')
Log(f"Init: Initializing {self.name}... failed")
Err(utils.HLine)
Log(error_msg)
Err(utils.HLine)
@@ -1106,7 +1151,7 @@ class DirMod(Mod):
@property
def init_mode(self) -> str:
return "exec" if (self.path / self.INIT_PY).exists() else ''
return "exec" if (self.path / self.INIT_PY).exists() else ""
@property
def name(self) -> str:
@@ -1126,7 +1171,9 @@ class DirMod(Mod):
if not self.supports_freecad_version():
self.state = ModState.Unsupported
Msg(f"NOTICE: {meta.Name} does not support this version of FreeCAD, so is being skipped")
Msg(
f"NOTICE: {meta.Name} does not support this version of FreeCAD, so is being skipped"
)
return
content = meta.Content
@@ -1134,10 +1181,16 @@ class DirMod(Mod):
workbenches = content["workbench"]
for workbench in workbenches:
if not workbench.supportsCurrentFreeCAD():
Msg(f"NOTICE: {meta.Name} content item {workbench.Name} does not support this version of FreeCAD, so is being skipped")
Msg(
f"NOTICE: {meta.Name} content item {workbench.Name} does not support this version of FreeCAD, so is being skipped"
)
continue
subdirectory = workbench.Name if not workbench.Subdirectory else workbench.Subdirectory
subdirectory = (
workbench.Name
if not workbench.Subdirectory
else workbench.Subdirectory
)
subdirectory = re.split(r"[/\\]+", subdirectory)
subdirectory = self.path / Path(*subdirectory)
@@ -1168,12 +1221,16 @@ class DirMod(Mod):
name = self.path.name
if name in Config.DisabledAddons:
Msg(f'NOTICE: Addon "{name}" disabled by presence of "--disable-addon {name}" argument')
Msg(
f'NOTICE: Addon "{name}" disabled by presence of "--disable-addon {name}" argument'
)
return True
for flag in (self.ALL_ADDONS_DISABLED, self.ADDON_DISABLED):
if (self.path / flag).exists():
Msg(f'NOTICE: Addon "{self.path!s}" disabled by presence of {flag} stopfile')
Msg(
f'NOTICE: Addon "{self.path!s}" disabled by presence of {flag} stopfile'
)
return True
return False
@@ -1198,19 +1255,21 @@ class DirMod(Mod):
init_py = self.path / self.INIT_PY
if not init_py.exists():
self.state = ModState.Loaded
Log(f"Init: Initializing {self.path!s} ({self.INIT_PY} not found)... ignore")
Log(
f"Init: Initializing {self.path!s} ({self.INIT_PY} not found)... ignore"
)
return
try:
source = init_py.read_text(encoding="utf-8")
code = compile(source, init_py, 'exec')
exec(code, {"__file__": str(init_py)})
code = compile(source, init_py, "exec")
exec(code, {**globals(), "__file__": str(init_py)})
except Exception as ex:
Log(f"Init: Initializing {self.path!s}... failed")
Log(utils.HLine)
Log(f"{traceback.format_exc()}")
Log(utils.HLine)
Err(f"During initialization the error \"{ex!s}\" occurred in {init_py!s}")
Err(f'During initialization the error "{ex!s}" occurred in {init_py!s}')
Err("Please look into the log file for further information")
self.state = ModState.Failed
else:
@@ -1231,15 +1290,20 @@ class ExtModScanner:
def scan(self):
import freecad
modules = (m[1] for m in pkgutil.iter_modules(freecad.__path__, "freecad.") if m[2])
modules = (
m[1] for m in pkgutil.iter_modules(freecad.__path__, "freecad.") if m[2]
)
for module_name in modules:
mod = ExtMod(module_name)
self.mods.append(mod)
if module_name in Config.DisabledAddons:
mod.state = ModState.Disabled
Msg(f'NOTICE: Addon "{module_name}" disabled by presence of "--disable-addon {module_name}" argument')
Msg(
f'NOTICE: Addon "{module_name}" disabled by presence of "--disable-addon {module_name}" argument'
)
continue
Log(f'Init: Initializing {module_name}')
Log(f"Init: Initializing {module_name}")
def iter(self) -> coll_abc.Iterable[ExtMod]:
return self.mods
@@ -1267,7 +1331,9 @@ class DirModScanner:
"""Paths of all discovered Mods."""
return [mod.path for mod in self.mods.values()]
def scan_and_override(self, base: Path, *, flat: bool = False, warning: str | None = None) -> None:
def scan_and_override(
self, base: Path, *, flat: bool = False, warning: str | None = None
) -> None:
"""
Scan in base with higher priority.
"""
@@ -1304,6 +1370,7 @@ class DirModScanner:
# │ Init Pipeline Definition │
# └────────────────────────────────────────────────┘
@transient
class InitPipeline:
"""
@@ -1318,7 +1385,9 @@ class InitPipeline:
# The library path is not strictly required, so if the OS itself raises an error when trying
# to resolve it, just fall back to something reasonable. See #26864.
std_lib = std_home / "lib"
Log(f"Resolving library directory '{App.getLibraryDir()}' failed, using fallback '{std_lib}'")
Log(
f"Resolving library directory '{App.getLibraryDir()}' failed, using fallback '{std_lib}'"
)
dir_mod_scanner = DirModScanner()
ext_mod_scanner = ExtModScanner()
search_paths = SearchPaths()
@@ -1351,7 +1420,9 @@ class InitPipeline:
legacy_user_mod = Path.home() / ".FreeCAD" / "Mod"
if legacy_user_mod.exists():
Wrn (f"User path has changed to {user_home!s}. Please move user modules and macros")
Wrn(
f"User path has changed to {user_home!s}. Please move user modules and macros"
)
# Libraries
libraries = PathSet()
@@ -1492,7 +1563,11 @@ class InitPipeline:
def setup_tty(self) -> None:
# Note: just checking whether stdin is a TTY is not enough, as the GUI is set up only after this
# script has run. And checking only the RunMode is not enough, as we are maybe not interactive.
if Config.RunMode == 'Cmd' and hasattr(sys.stdin, 'isatty') and sys.stdin.isatty():
if (
Config.RunMode == "Cmd"
and hasattr(sys.stdin, "isatty")
and sys.stdin.isatty()
):
utils.setup_tty_tab_completion()
def report(self) -> None:
@@ -1510,12 +1585,16 @@ class InitPipeline:
output.append(output[0])
for mod in self.dir_mod_scanner.iter():
output.append(f"| {mod.name:<24.24} | {mod.state.name:<10.10} | {mod.init_mode:<6.6} | {mod.path!s}")
output.append(
f"| {mod.name:<24.24} | {mod.state.name:<10.10} | {mod.init_mode:<6.6} | {mod.path!s}"
)
for alt in mod.alternative_paths:
output.append(f"| {' ':<24.24} | {' ':<10.10} | {' ':<6.6} | {alt!s}")
for mod in self.ext_mod_scanner.iter():
output.append(f"| {mod.name:<24.24} | {mod.state.name:<10.10} | {mod.init_mode:<6.6} | {mod.name}")
output.append(
f"| {mod.name:<24.24} | {mod.state.name:<10.10} | {mod.init_mode:<6.6} | {mod.name}"
)
for line in output:
Log(line)
@@ -1537,14 +1616,15 @@ class InitPipeline:
# │ Init Applications │
# └────────────────────────────────────────────────┘
@transient
@call_in_place
def init_applications() -> None:
try:
InitPipeline().run()
Log('Init: App::FreeCADInit.py done')
Log("Init: App::FreeCADInit.py done")
except Exception as ex:
Err(f'Error in init_applications {ex!s}')
Err(f"Error in init_applications {ex!s}")
Err(utils.HLine)
Err(traceback.format_exc())
Err(utils.HLine)

View File

@@ -35,13 +35,14 @@
# | >>>> | FreeCADGuiInit | only if Gui is up |
# +------+------------------+-----------------------------+
from enum import IntEnum, Enum
from dataclasses import dataclass
import importlib
import re
import traceback
import typing
import re
from dataclasses import dataclass
from enum import Enum, IntEnum
from pathlib import Path
import importlib
import FreeCAD
import FreeCADGui
@@ -104,11 +105,15 @@ class Workbench:
ToolTip = ""
Icon = None
__Workbench__: "Workbench" # Injected by FreeCAD, see: Application::activateWorkbench
__Workbench__: (
"Workbench" # Injected by FreeCAD, see: Application::activateWorkbench
)
def Initialize(self):
"""Initializes this workbench."""
App.Console.PrintWarning(f"{self!s}: Workbench.Initialize() not implemented in subclass!")
App.Console.PrintWarning(
f"{self!s}: Workbench.Initialize() not implemented in subclass!"
)
def ContextMenu(self, recipient):
pass
@@ -295,20 +300,24 @@ class DirModGui(ModGui):
try:
source = init_gui_py.read_text(encoding="utf-8")
code = compile(source, init_gui_py, "exec")
exec(code, {"__file__": str(init_gui_py)})
exec(code, {**globals(), "__file__": str(init_gui_py)})
except Exception as ex:
sep = "-" * 100 + "\n"
Log(f"Init: Initializing {target!s}... failed\n")
Log(sep)
Log(traceback.format_exc())
Log(sep)
Err(f'During initialization the error "{ex!s}" occurred in {init_gui_py!s}\n')
Err(
f'During initialization the error "{ex!s}" occurred in {init_gui_py!s}\n'
)
Err("Look into the log file for further information\n")
else:
Log(f"Init: Initializing {target!s}... done\n")
return True
else:
Log(f"Init: Initializing {target!s} (InitGui.py not found)... ignore\n")
Log(
f"Init: Initializing {target!s} (InitGui.py not found)... ignore\n"
)
return False
def process_metadata(self) -> bool:
@@ -325,7 +334,9 @@ class DirModGui(ModGui):
if not workbench_metadata.supportsCurrentFreeCAD():
continue
subdirectory = workbench_metadata.Subdirectory or workbench_metadata.Name
subdirectory = (
workbench_metadata.Subdirectory or workbench_metadata.Name
)
subdirectory = self.mod.path / Path(*re.split(r"[/\\]+", subdirectory))
if not subdirectory.exists():
continue
@@ -373,7 +384,9 @@ class ExtModGui(ModGui):
Err(f'During initialization the error "{ex!s}" occurred\n')
except Exception as ex:
sep = "-" * 80 + "\n"
Err(f'During initialization the error "{ex!s}" occurred in {self.mod.name}\n')
Err(
f'During initialization the error "{ex!s}" occurred in {self.mod.name}\n'
)
Err(sep)
Err(traceback.format_exc())
Err(sep)
@@ -400,9 +413,7 @@ def InitApplications():
gui = mod_type(mod)
gui.load()
if mod.init_mode:
row = (
f"| {mod.name:<24.24} | {mod.state.name:<10.10} | {mod.init_mode:<6.6} |\n"
)
row = f"| {mod.name:<24.24} | {mod.state.name:<10.10} | {mod.init_mode:<6.6} |\n"
output.append(row)
output = []
@@ -457,7 +468,9 @@ FreeCAD.addImportType("Inventor V2.1 (*.iv *.IV)", "FreeCADGui")
FreeCAD.addImportType(
"VRML V2.0 (*.wrl *.WRL *.vrml *.VRML *.wrz *.WRZ *.wrl.gz *.WRL.GZ)", "FreeCADGui"
)
FreeCAD.addImportType("Python (*.py *.FCMacro *.FCScript *.fcmacro *.fcscript)", "FreeCADGui")
FreeCAD.addImportType(
"Python (*.py *.FCMacro *.FCScript *.fcmacro *.fcscript)", "FreeCADGui"
)
FreeCAD.addExportType("Inventor V2.1 (*.iv)", "FreeCADGui")
FreeCAD.addExportType("VRML V2.0 (*.wrl *.vrml *.wrz *.wrl.gz)", "FreeCADGui")
FreeCAD.addExportType("X3D Extensible 3D (*.x3d *.x3dz)", "FreeCADGui")