Merge pull request 'fix: preserve caller globals in exec() for module Init.py/InitGui.py loading' (#240) from fix/exec-globals-regression into main
Some checks failed
Build and Test / build (push) Has been cancelled

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

View File

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