1555 lines
54 KiB
Python
1555 lines
54 KiB
Python
#***************************************************************************
|
|
#* 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
|
|
#
|
|
# Gathering all the information to start FreeCAD.
|
|
# This is the second of four init scripts:
|
|
# +------+------------------+-----------------------------+
|
|
# | This | Script | Runs |
|
|
# +------+------------------+-----------------------------+
|
|
# | | CMakeVariables | always |
|
|
# | >>>> | FreeCADInit | always |
|
|
# | | FreeCADTest | only if test and not Gui |
|
|
# | | FreeCADGuiInit | only if Gui is up |
|
|
# +------+------------------+-----------------------------+
|
|
|
|
import FreeCAD
|
|
|
|
App = FreeCAD
|
|
|
|
App.Console.PrintLog("Init: starting App::FreeCADInit.py\n")
|
|
App.Console.PrintLog("░░░▀█▀░█▀█░▀█▀░▀█▀░░░█▀█░█▀█░█▀█░░\n")
|
|
App.Console.PrintLog("░░░░█░░█░█░░█░░░█░░░░█▀█░█▀▀░█▀▀░░\n")
|
|
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 functools
|
|
import re
|
|
import pkgutil
|
|
except ImportError:
|
|
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)
|
|
Wrn = __logger(App.Console.PrintWarning)
|
|
Crt = __logger(App.Console.PrintCritical)
|
|
Ntf = __logger(App.Console.PrintNotification)
|
|
Tnf = __logger(App.Console.PrintTranslatedNotification)
|
|
|
|
|
|
class FCADLogger:
|
|
"""
|
|
Convenient class for tagged logging.
|
|
|
|
Example usage:
|
|
>>> logger = FreeCAD.Logger('MyModule')
|
|
>>> logger.info('log test {}',1)
|
|
24.36053 <MyModule> <input>(1): test log 1
|
|
|
|
The default output format is:
|
|
<timestamp> <tag> <source file>(line number): message
|
|
|
|
The message is formatted using new style Python string formatting, e.g.
|
|
'test {}'.format(1). It is strongly recommended to not directly use
|
|
Python string formatting, but pass additional argument indirectly through
|
|
various logger print function, because the logger can skip string
|
|
evaluation in case the logging level is disabled. For more options,
|
|
please consult the docstring of __init__(), catch() and report().
|
|
|
|
To set/get logger level:
|
|
>>> FreeCAD.setLogLevel('MyModule','Trace')
|
|
>>> FreeCAD.getLogLevel('MyModule')
|
|
4
|
|
|
|
There are five predefined logger level, each corresponding to an integer
|
|
value, as shown below together with the corresponding logger print
|
|
method,
|
|
0: Error, Logger.error()
|
|
1: Warning, Logger.warn()
|
|
2: Message, Logger.msg() or info()
|
|
3: Log, Logger.log() or debug()
|
|
4: Trace, Logger.trace()
|
|
|
|
FreeCAD.setLogLevel() supports both text and integer value, which allows
|
|
you to define your own levels. The level set is persisted to user
|
|
configuration file.
|
|
|
|
By default any tag has a log level of 2 for release, and 3 for debug
|
|
build.
|
|
"""
|
|
|
|
_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,
|
|
}
|
|
|
|
_printer = (
|
|
App.Console.PrintError,
|
|
App.Console.PrintWarning,
|
|
App.Console.PrintMessage,
|
|
App.Console.PrintLog,
|
|
App.Console.PrintLog
|
|
)
|
|
|
|
_defaults = (
|
|
('printTag', True),
|
|
('noUpdateUI', True),
|
|
('timing', True),
|
|
('lineno', True),
|
|
('parent', None),
|
|
('title', 'FreeCAD'),
|
|
)
|
|
|
|
printTag: bool
|
|
noUpdateUI: bool
|
|
timing: bool
|
|
lineno: bool
|
|
parent: "FCADLogger"
|
|
title: str
|
|
|
|
def __init__(self, tag: str, **kwargs) -> None:
|
|
"""
|
|
Construct a logger instance.
|
|
|
|
Supported arguments are their default values are,
|
|
|
|
* tag: a string tag for this logger. The log level of this logger can be
|
|
accessed using FreeCAD.getLogLevel(tag)/setLogLevel(tag,level).
|
|
All logger instance with the same tag shares the same level
|
|
setting.
|
|
|
|
* printTag (True): whether to print tag
|
|
|
|
* noUpdateUI (True): whether to update GUI when printing. This is useful
|
|
to show log output on lengthy operations. Be
|
|
careful though, this may allow unexpected user
|
|
interaction when the application is busy, which may
|
|
lead to crash
|
|
|
|
* timing (True): whether to print time stamp
|
|
|
|
* lineno (True): whether to print source file and line number
|
|
|
|
* parent (None): provide a parent logger, so that the log printing will
|
|
check for parent's log level in addition of its own
|
|
|
|
* title ('FreeCAD'): message box title used by report()
|
|
"""
|
|
self.tag = tag
|
|
self.laststamp = datetime.now()
|
|
for key, default in self._defaults:
|
|
setattr(self, key, kwargs.get(key, default))
|
|
|
|
def _isEnabledFor(self, level: int) -> bool:
|
|
"""
|
|
Internal function to check for an integer log level.
|
|
|
|
* level: integer log level
|
|
"""
|
|
if self.parent and not self.parent._isEnabledFor(level):
|
|
return False
|
|
return App.getLogLevel(self.tag) >= level
|
|
|
|
def isEnabledFor(self, level: int | str) -> bool:
|
|
"""
|
|
To check for an integer or text log level.
|
|
|
|
* level: integer or text log level
|
|
"""
|
|
if not isinstance(level, int):
|
|
level = self.__class__._levels[level]
|
|
return self._isEnabledFor(level)
|
|
|
|
def _logger_method(name: str, level: int, level_name: str): # pylint: disable=no-self-argument
|
|
"""
|
|
Create level logger.
|
|
"""
|
|
docstring = f"""
|
|
"{level_name}" level log printer
|
|
|
|
* msg: message string. May contain new style Python string formatter.
|
|
|
|
This function accepts additional positional and keyword arguments,
|
|
which are forward to string.format() to generate the logging
|
|
message. It is strongly recommended to not directly use Python
|
|
string formatting, but pass additional arguments here, because the
|
|
printer can skip string evaluation in case the logging level is
|
|
disabled.
|
|
"""
|
|
|
|
def log_fn(self, msg: str, *args, **kwargs) -> None:
|
|
if self._isEnabledFor(level):
|
|
frame = kwargs.pop('frame', 0) + 1
|
|
self._log(level, msg, frame, args, kwargs)
|
|
|
|
log_fn.__doc__ = docstring
|
|
log_fn.__name__ = name
|
|
return log_fn
|
|
|
|
def _log(
|
|
self,
|
|
level: int,
|
|
msg: str,
|
|
frame: int = 0,
|
|
args: tuple = (),
|
|
kwargs: dict | None = None,
|
|
) -> None:
|
|
"""
|
|
Internal log printing function.
|
|
|
|
* level: integer log level
|
|
|
|
* msg: message, may contain new style string format specifier
|
|
|
|
* frame (0): the calling frame for printing source file and line
|
|
number. For example, in case you have your own logging
|
|
function, and you want to show the callers source
|
|
location, then set frame to one.
|
|
|
|
* args: tuple for positional arguments to be passed to
|
|
string.format()
|
|
|
|
* kwargs: dictionary for keyword arguments to be passed to
|
|
string.format()
|
|
"""
|
|
|
|
if (args or kwargs) and isinstance(msg, str):
|
|
if not kwargs:
|
|
msg = msg.format(*args)
|
|
else:
|
|
msg = msg.format(*args, **kwargs)
|
|
|
|
prefix = ''
|
|
|
|
if self.timing:
|
|
now = datetime.now()
|
|
prefix += '{} '.format((now-self.laststamp).total_seconds())
|
|
self.laststamp = now
|
|
|
|
if self.printTag:
|
|
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)
|
|
except Exception:
|
|
frame = inspect.stack()[frame+1]
|
|
prefix += '{}({}): '.format(os.path.basename(frame[1]),frame[2])
|
|
|
|
self.__class__._printer[level]('{}{}\n'.format(prefix,msg))
|
|
|
|
if not self.noUpdateUI and App.GuiUp:
|
|
import FreeCADGui
|
|
try:
|
|
FreeCADGui.updateGui()
|
|
except Exception:
|
|
pass
|
|
|
|
def _catch_logger_method(name: str, level: int, level_name: str): # pylint: disable=no-self-argument
|
|
"""
|
|
Create level catch logger.
|
|
"""
|
|
docstring = f"""
|
|
Catch any exception from a function and print as "{level_name}".
|
|
|
|
* msg: message string. Unlike log printer, this argument must not
|
|
contain any string formatter.
|
|
|
|
* func: a callable object
|
|
|
|
* args: tuple of positional arguments to be passed to func.
|
|
|
|
* kwargs: dictionary of keyword arguments to be passed to func.
|
|
"""
|
|
|
|
def catch_fn(self, msg: str, func: callable, *args, **kwargs) -> object | None:
|
|
return self._catch(level, msg, func, args, kwargs)
|
|
|
|
catch_fn.__doc__ = docstring
|
|
catch_fn.__name__ = name
|
|
return catch_fn
|
|
|
|
def _catch(
|
|
self,
|
|
level: int,
|
|
msg: str,
|
|
func: callable,
|
|
args: tuple = (),
|
|
kwargs: dict | None = None,
|
|
) -> object | None:
|
|
"""
|
|
Internal function to log exception of any callable.
|
|
|
|
* level: integer log level
|
|
|
|
* msg: message string. Unlike _log(), this argument must not contain
|
|
any string formatter.
|
|
|
|
* func: a callable object
|
|
|
|
* args: tuple of positional arguments to be passed to func.
|
|
|
|
* kwargs: dictionary of keyword arguments to be passed to func.
|
|
"""
|
|
try:
|
|
if not kwargs:
|
|
kwargs = {}
|
|
return func(*args, **kwargs)
|
|
except Exception:
|
|
if self._isEnabledFor(level):
|
|
self._log(level, f"{msg}\n{traceback.format_exc()}", frame=2)
|
|
return None
|
|
|
|
def report(self, msg: str, func: callable, *args, **kwargs) -> object | None:
|
|
"""
|
|
Catch any exception report it with a message box.
|
|
|
|
* msg: message string. Unlike log printer, this argument must not
|
|
contain any string formatter.
|
|
|
|
* func: a callable object
|
|
|
|
* args: tuple of positional arguments to be passed to func.
|
|
|
|
* kwargs: dictionary of keyword arguments to be passed to func.
|
|
"""
|
|
try:
|
|
return func(*args, **kwargs)
|
|
except Exception as e:
|
|
self.error(f"{msg}\n{traceback.format_exc()}", frame=1)
|
|
if App.GuiUp:
|
|
import FreeCADGui, PySide
|
|
PySide.QtGui.QMessageBox.critical(
|
|
FreeCADGui.getMainWindow(),
|
|
self.title,
|
|
str(e),
|
|
)
|
|
return None
|
|
|
|
error = _logger_method("error", 0, "Error")
|
|
warn = _logger_method("warn", 1, "Warning")
|
|
msg = _logger_method("msg", 2, "Message")
|
|
log = _logger_method("log", 3, "Log")
|
|
trace = _logger_method("trace", 4, "Trace")
|
|
info = msg
|
|
debug = log
|
|
|
|
catch = _catch_logger_method("catch", 0, "Error")
|
|
catchWarn = _catch_logger_method("catchWarn", 1, "Warning")
|
|
catchMsg = _catch_logger_method("catchMsg", 2, "Message")
|
|
catchLog = _catch_logger_method("catchLog", 3, "Log")
|
|
catchTrace = _catch_logger_method("catchTrace", 4, "Trace")
|
|
catchInfo = catchMsg
|
|
catchDebug = catchLog
|
|
|
|
|
|
App.Logger = FCADLogger
|
|
|
|
|
|
# ┌────────────────────────────────────────────────┐
|
|
# │ App definitions │
|
|
# └────────────────────────────────────────────────┘
|
|
|
|
# store the cmake variables
|
|
# This data comes from generated file src/App/CMakeScript.h and it is
|
|
# injected into globals in a previous stage.
|
|
App.__cmake__ = globals().get("cmake", [])
|
|
|
|
# store unit test names
|
|
App.__unit_test__ = []
|
|
|
|
App.addImportType("FreeCAD document (*.FCStd)", "FreeCAD")
|
|
|
|
# set to no gui, is overwritten by InitGui
|
|
App.GuiUp = 0
|
|
|
|
# fmt: off
|
|
# fill up unit definitions
|
|
|
|
App.Units.NanoMetre = App.Units.Quantity('nm')
|
|
App.Units.MicroMetre = App.Units.Quantity('um')
|
|
App.Units.MilliMetre = App.Units.Quantity('mm')
|
|
App.Units.CentiMetre = App.Units.Quantity('cm')
|
|
App.Units.DeciMetre = App.Units.Quantity('dm')
|
|
App.Units.Metre = App.Units.Quantity('m')
|
|
App.Units.KiloMetre = App.Units.Quantity('km')
|
|
|
|
App.Units.MilliLiter = App.Units.Quantity('ml')
|
|
App.Units.Liter = App.Units.Quantity('l')
|
|
|
|
App.Units.Hertz = App.Units.Quantity('Hz')
|
|
App.Units.KiloHertz = App.Units.Quantity('kHz')
|
|
App.Units.MegaHertz = App.Units.Quantity('MHz')
|
|
App.Units.GigaHertz = App.Units.Quantity('GHz')
|
|
App.Units.TeraHertz = App.Units.Quantity('THz')
|
|
|
|
App.Units.MicroGram = App.Units.Quantity('ug')
|
|
App.Units.MilliGram = App.Units.Quantity('mg')
|
|
App.Units.Gram = App.Units.Quantity('g')
|
|
App.Units.KiloGram = App.Units.Quantity('kg')
|
|
App.Units.Ton = App.Units.Quantity('t')
|
|
|
|
App.Units.Second = App.Units.Quantity('s')
|
|
App.Units.Minute = App.Units.Quantity('min')
|
|
App.Units.Hour = App.Units.Quantity('h')
|
|
|
|
App.Units.Ampere = App.Units.Quantity('A')
|
|
App.Units.MilliAmpere = App.Units.Quantity('mA')
|
|
App.Units.KiloAmpere = App.Units.Quantity('kA')
|
|
App.Units.MegaAmpere = App.Units.Quantity('MA')
|
|
|
|
App.Units.Kelvin = App.Units.Quantity('K')
|
|
App.Units.MilliKelvin = App.Units.Quantity('mK')
|
|
App.Units.MicroKelvin = App.Units.Quantity('uK')
|
|
|
|
App.Units.MilliMole = App.Units.Quantity('mmol')
|
|
App.Units.Mole = App.Units.Quantity('mol')
|
|
|
|
App.Units.Candela = App.Units.Quantity('cd')
|
|
|
|
App.Units.Inch = App.Units.Quantity('in')
|
|
App.Units.Foot = App.Units.Quantity('ft')
|
|
App.Units.Thou = App.Units.Quantity('thou')
|
|
App.Units.Yard = App.Units.Quantity('yd')
|
|
App.Units.Mile = App.Units.Quantity('mi')
|
|
|
|
App.Units.SquareFoot = App.Units.Quantity('sqft')
|
|
App.Units.CubicFoot = App.Units.Quantity('cft')
|
|
|
|
App.Units.Pound = App.Units.Quantity('lb')
|
|
App.Units.Ounce = App.Units.Quantity('oz')
|
|
App.Units.Stone = App.Units.Quantity('st')
|
|
App.Units.Hundredweights= App.Units.Quantity('cwt')
|
|
|
|
App.Units.Newton = App.Units.Quantity('N')
|
|
App.Units.MilliNewton = App.Units.Quantity('mN')
|
|
App.Units.KiloNewton = App.Units.Quantity('kN')
|
|
App.Units.MegaNewton = App.Units.Quantity('MN')
|
|
|
|
App.Units.NewtonPerMeter = App.Units.Quantity('N/m')
|
|
App.Units.MilliNewtonPerMeter = App.Units.Quantity('mN/m')
|
|
App.Units.KiloNewtonPerMeter = App.Units.Quantity('kN/m')
|
|
App.Units.MegaNewtonPerMeter = App.Units.Quantity('MN/m')
|
|
|
|
App.Units.Pascal = App.Units.Quantity('Pa')
|
|
App.Units.KiloPascal = App.Units.Quantity('kPa')
|
|
App.Units.MegaPascal = App.Units.Quantity('MPa')
|
|
App.Units.GigaPascal = App.Units.Quantity('GPa')
|
|
|
|
App.Units.MilliBar = App.Units.Quantity('mbar')
|
|
App.Units.Bar = App.Units.Quantity('bar')
|
|
|
|
App.Units.PoundForce = App.Units.Quantity('lbf')
|
|
App.Units.Torr = App.Units.Quantity('Torr')
|
|
App.Units.mTorr = App.Units.Quantity('mTorr')
|
|
App.Units.yTorr = App.Units.Quantity('uTorr')
|
|
|
|
App.Units.PSI = App.Units.Quantity('psi')
|
|
App.Units.KSI = App.Units.Quantity('ksi')
|
|
App.Units.MPSI = App.Units.Quantity('Mpsi')
|
|
|
|
App.Units.Watt = App.Units.Quantity('W')
|
|
App.Units.MilliWatt = App.Units.Quantity('mW')
|
|
App.Units.KiloWatt = App.Units.Quantity('kW')
|
|
App.Units.VoltAmpere = App.Units.Quantity('VA')
|
|
|
|
App.Units.Volt = App.Units.Quantity('V')
|
|
App.Units.MilliVolt = App.Units.Quantity('mV')
|
|
App.Units.KiloVolt = App.Units.Quantity('kV')
|
|
|
|
App.Units.MegaSiemens = App.Units.Quantity('MS')
|
|
App.Units.KiloSiemens = App.Units.Quantity('kS')
|
|
App.Units.Siemens = App.Units.Quantity('S')
|
|
App.Units.MilliSiemens = App.Units.Quantity('mS')
|
|
App.Units.MicroSiemens = App.Units.Quantity('uS')
|
|
|
|
App.Units.Ohm = App.Units.Quantity('Ohm')
|
|
App.Units.KiloOhm = App.Units.Quantity('kOhm')
|
|
App.Units.MegaOhm = App.Units.Quantity('MOhm')
|
|
|
|
App.Units.Coulomb = App.Units.Quantity('C')
|
|
|
|
App.Units.Tesla = App.Units.Quantity('T')
|
|
App.Units.Gauss = App.Units.Quantity('G')
|
|
|
|
App.Units.Weber = App.Units.Quantity('Wb')
|
|
|
|
# disable Oersted because people need to input e.g. a field strength of
|
|
# 1 ampere per meter -> 1 A/m and not get the recalculation to Oersted
|
|
# App.Units.Oersted = App.Units.Quantity('Oe')
|
|
|
|
App.Units.PicoFarad = App.Units.Quantity('pF')
|
|
App.Units.NanoFarad = App.Units.Quantity('nF')
|
|
App.Units.MicroFarad = App.Units.Quantity('uF')
|
|
App.Units.MilliFarad = App.Units.Quantity('mF')
|
|
App.Units.Farad = App.Units.Quantity('F')
|
|
|
|
App.Units.NanoHenry = App.Units.Quantity('nH')
|
|
App.Units.MicroHenry = App.Units.Quantity('uH')
|
|
App.Units.MilliHenry = App.Units.Quantity('mH')
|
|
App.Units.Henry = App.Units.Quantity('H')
|
|
|
|
App.Units.Joule = App.Units.Quantity('J')
|
|
App.Units.MilliJoule = App.Units.Quantity('mJ')
|
|
App.Units.KiloJoule = App.Units.Quantity('kJ')
|
|
App.Units.NewtonMeter = App.Units.Quantity('Nm')
|
|
App.Units.VoltAmpereSecond = App.Units.Quantity('VAs')
|
|
App.Units.WattSecond = App.Units.Quantity('Ws')
|
|
App.Units.KiloWattHour = App.Units.Quantity('kWh')
|
|
App.Units.ElectronVolt = App.Units.Quantity('eV')
|
|
App.Units.KiloElectronVolt = App.Units.Quantity('keV')
|
|
App.Units.MegaElectronVolt = App.Units.Quantity('MeV')
|
|
App.Units.Calorie = App.Units.Quantity('cal')
|
|
App.Units.KiloCalorie = App.Units.Quantity('kcal')
|
|
|
|
App.Units.MPH = App.Units.Quantity('mi/h')
|
|
App.Units.KMH = App.Units.Quantity('km/h')
|
|
|
|
App.Units.Degree = App.Units.Quantity('deg')
|
|
App.Units.Radian = App.Units.Quantity('rad')
|
|
App.Units.Gon = App.Units.Quantity('gon')
|
|
App.Units.AngularMinute = App.Units.Quantity().AngularMinute
|
|
App.Units.AngularSecond = App.Units.Quantity().AngularSecond
|
|
|
|
|
|
# SI base units
|
|
# (length, weight, time, current, temperature, amount of substance, luminous intensity, angle)
|
|
App.Units.AmountOfSubstance = App.Units.Unit(0,0,0,0,0,1)
|
|
App.Units.ElectricCurrent = App.Units.Unit(0,0,0,1)
|
|
App.Units.Length = App.Units.Unit(1)
|
|
App.Units.LuminousIntensity = App.Units.Unit(0,0,0,0,0,0,1)
|
|
App.Units.Mass = App.Units.Unit(0,1)
|
|
App.Units.Temperature = App.Units.Unit(0,0,0,0,1)
|
|
App.Units.TimeSpan = App.Units.Unit(0,0,1)
|
|
|
|
# all other combined units
|
|
App.Units.Acceleration = App.Units.Unit(1,0,-2)
|
|
App.Units.Angle = App.Units.Unit(0,0,0,0,0,0,0,1)
|
|
App.Units.AngleOfFriction = App.Units.Unit(0,0,0,0,0,0,0,1)
|
|
App.Units.Area = App.Units.Unit(2)
|
|
App.Units.CompressiveStrength = App.Units.Unit(-1,1,-2)
|
|
App.Units.CurrentDensity = App.Units.Unit(-2,0,0,1)
|
|
App.Units.Density = App.Units.Unit(-3,1)
|
|
App.Units.DissipationRate = App.Units.Unit(2,0,-3)
|
|
App.Units.DynamicViscosity = App.Units.Unit(-1,1,-1)
|
|
App.Units.Frequency = App.Units.Unit(0,0,-1)
|
|
App.Units.MagneticFluxDensity = App.Units.Unit(0,1,-2,-1)
|
|
App.Units.Magnetization = App.Units.Unit(-1,0,0,1)
|
|
App.Units.ElectricalCapacitance = App.Units.Unit(-2,-1,4,2)
|
|
App.Units.ElectricalConductance = App.Units.Unit(-2,-1,3,2)
|
|
App.Units.ElectricalConductivity = App.Units.Unit(-3,-1,3,2)
|
|
App.Units.ElectricalInductance = App.Units.Unit(2,1,-2,-2)
|
|
App.Units.ElectricalResistance = App.Units.Unit(2,1,-3,-2)
|
|
App.Units.ElectricCharge = App.Units.Unit(0,0,1,1)
|
|
App.Units.ElectricPotential = App.Units.Unit(2,1,-3,-1)
|
|
App.Units.Force = App.Units.Unit(1,1,-2)
|
|
App.Units.HeatFlux = App.Units.Unit(0,1,-3,0,0)
|
|
App.Units.InverseArea = App.Units.Unit(-2)
|
|
App.Units.InverseLength = App.Units.Unit(-1)
|
|
App.Units.InverseVolume = App.Units.Unit(-3)
|
|
App.Units.KinematicViscosity = App.Units.Unit(2,0,-1)
|
|
App.Units.Pressure = App.Units.Unit(-1,1,-2)
|
|
App.Units.Power = App.Units.Unit(2,1,-3)
|
|
App.Units.ShearModulus = App.Units.Unit(-1,1,-2)
|
|
App.Units.SpecificEnergy = App.Units.Unit(2,0,-2)
|
|
App.Units.SpecificHeat = App.Units.Unit(2,0,-2,0,-1)
|
|
App.Units.Stiffness = App.Units.Unit(0,1,-2)
|
|
App.Units.Stress = App.Units.Unit(-1,1,-2)
|
|
App.Units.ThermalConductivity = App.Units.Unit(1,1,-3,0,-1)
|
|
App.Units.ThermalExpansionCoefficient = App.Units.Unit(0,0,0,0,-1)
|
|
App.Units.ThermalTransferCoefficient = App.Units.Unit(0,1,-3,0,-1)
|
|
App.Units.UltimateTensileStrength = App.Units.Unit(-1,1,-2)
|
|
App.Units.Velocity = App.Units.Unit(1,0,-1)
|
|
App.Units.VacuumPermittivity = App.Units.Unit(-3,-1,4,2)
|
|
App.Units.Volume = App.Units.Unit(3)
|
|
App.Units.VolumeFlowRate = App.Units.Unit(3,0,-1)
|
|
App.Units.VolumetricThermalExpansionCoefficient = App.Units.Unit(0,0,0,0,-1)
|
|
App.Units.Work = App.Units.Unit(2,1,-2)
|
|
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):
|
|
Internal = 0
|
|
MKS = 1
|
|
Imperial = 2
|
|
ImperialDecimal = 3
|
|
Centimeter = 4
|
|
ImperialBuilding = 5
|
|
MmMin = 6
|
|
ImperialCivil = 7
|
|
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
|
|
NonUniformRight = 1
|
|
NonUniformLeft = 2
|
|
Uniform = 3
|
|
|
|
App.ScaleType = ScaleType
|
|
|
|
class PropertyType(IntEnum):
|
|
Prop_None = 0
|
|
Prop_ReadOnly = 1
|
|
Prop_Transient = 2
|
|
Prop_Hidden = 4
|
|
Prop_Output = 8
|
|
Prop_NoRecompute = 16
|
|
Prop_NoPersist = 32
|
|
|
|
App.PropertyType = PropertyType
|
|
|
|
class ReturnType(IntEnum):
|
|
PyObject = 0
|
|
DocObject = 1
|
|
DocAndPyObject = 2
|
|
Placement = 3
|
|
Matrix = 4
|
|
LinkAndPlacement = 5
|
|
LinkAndMatrix = 6
|
|
|
|
App.ReturnType = ReturnType
|
|
|
|
|
|
# ┌────────────────────────────────────────────────┐
|
|
# │ Init Framework │
|
|
# └────────────────────────────────────────────────┘
|
|
|
|
class Transient:
|
|
"""
|
|
Mark the symbol for removal from global scope on cleanup.
|
|
"""
|
|
|
|
names = ["Path"]
|
|
|
|
def __call__(self, target):
|
|
Transient.names.append(target.__name__)
|
|
return target
|
|
|
|
@classmethod
|
|
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)]
|
|
for name in names:
|
|
if name not in keep:
|
|
del globals()[name]
|
|
|
|
# Remove transient symbols
|
|
cls.names.extend(("transient", cls.__name__))
|
|
for name in cls.names:
|
|
del globals()[name]
|
|
|
|
|
|
transient = Transient()
|
|
|
|
|
|
@transient
|
|
def call_in_place(fn):
|
|
"""Call the function in place immediately after its definition."""
|
|
fn()
|
|
return fn
|
|
|
|
@transient
|
|
class utils:
|
|
HLine = "-" * 80
|
|
|
|
@staticmethod
|
|
def str_to_paths(paths: str, delim: str = ";") -> list[Path]:
|
|
"""Convert a delimited string list of paths to a list of Path objects."""
|
|
items = (item.strip() for item in paths.split(delim))
|
|
# Filtering out empty paths: This may break backwards compat or not.
|
|
# If something breaks, just remove the filter to allow empty paths
|
|
non_empty_paths = filter(bool, items)
|
|
return list(map(Path, non_empty_paths))
|
|
|
|
@staticmethod
|
|
def env_to_path(name: str) -> Path | None:
|
|
if path := os.environ.get(name):
|
|
return Path(path)
|
|
return None
|
|
|
|
@staticmethod
|
|
def setup_tty_tab_completion():
|
|
"""
|
|
Tries to setup readline-based tab-completion.
|
|
|
|
Call this function only if you are in a tty-based REPL environment.
|
|
"""
|
|
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,
|
|
# we just ignore import errors here.
|
|
pass
|
|
|
|
|
|
@transient
|
|
class PathPriority(IntEnum):
|
|
Ignore = 0
|
|
FallbackLast = 1
|
|
FallbackFirst = 2
|
|
OverrideLast = 3
|
|
OverrideFirst = 4
|
|
|
|
@transient
|
|
@dataclasses.dataclass
|
|
class PathSet:
|
|
"""
|
|
Collection of paths with priority support.
|
|
|
|
Items can be inserted at specific priorities and composed in nested levels.
|
|
|
|
Structure:
|
|
[
|
|
override:(OverrideFirst, *_, OverrideLast),
|
|
*source,
|
|
fallback:(FallbackFirst, *_, FallbackLast),
|
|
]
|
|
"""
|
|
|
|
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)
|
|
|
|
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()
|
|
if not item.exists():
|
|
return
|
|
|
|
if priority == PathPriority.FallbackLast:
|
|
self.fallback.append(item)
|
|
elif priority == PathPriority.OverrideFirst:
|
|
self.override.appendleft(item)
|
|
elif priority == PathPriority.OverrideLast:
|
|
self.override.append(item)
|
|
elif priority == PathPriority.FallbackFirst:
|
|
self.fallback.appendleft(item)
|
|
elif priority == PathPriority.Ignore:
|
|
pass
|
|
else:
|
|
msg = "Invalid path priority"
|
|
raise ValueError(msg)
|
|
|
|
def iter(self) -> coll_abc.Iterable[Path]:
|
|
"""
|
|
Return iterable in priority order, higher first.
|
|
"""
|
|
for section in (self.override, self.source, self.fallback):
|
|
for path in section:
|
|
if isinstance(path, PathSet):
|
|
yield from path.iter()
|
|
else:
|
|
yield path
|
|
|
|
def build(self) -> list[Path]:
|
|
"""
|
|
Build and remove duplicates, keep priority order.
|
|
"""
|
|
return list(dict.fromkeys(self.iter()))
|
|
|
|
@transient
|
|
class SearchPaths:
|
|
"""
|
|
Manages search paths for binaries, libraries and modules.
|
|
|
|
The most generic search path is PATH in environment.
|
|
DLL search path is windows specific.
|
|
sys.path is for module imports.
|
|
"""
|
|
|
|
env_path: PathSet
|
|
sys_path: PathSet
|
|
dll_path: PathSet
|
|
|
|
def __init__(self):
|
|
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()
|
|
|
|
def add(
|
|
self,
|
|
item: Path | PathSet,
|
|
*,
|
|
env_path: PathPriority = PathPriority.OverrideLast,
|
|
sys_path: PathPriority = PathPriority.OverrideFirst,
|
|
dll_path: PathPriority = PathPriority.OverrideLast
|
|
) -> None:
|
|
"""
|
|
Add item to required namespaces with the specified priority.
|
|
|
|
Actual changes are buffered until commit().
|
|
"""
|
|
self.env_path.add(item, env_path)
|
|
self.sys_path.add(item, sys_path)
|
|
self.dll_path.add(item, dll_path)
|
|
|
|
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())
|
|
sys.path = [str(path) for path in self.sys_path.build()]
|
|
|
|
if win32 := WindowsPlatform():
|
|
win32.add_dll_search_paths(self.dll_path.build())
|
|
|
|
# Reset
|
|
self.__init__()
|
|
|
|
|
|
@transient
|
|
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)
|
|
|
|
|
|
@transient
|
|
class WindowsPlatform:
|
|
"""
|
|
Windows specific hooks.
|
|
"""
|
|
|
|
initialized = False
|
|
enabled = platform.system() == 'Windows' and hasattr(os, "add_dll_directory")
|
|
|
|
def __init__(self) -> None:
|
|
if not WindowsPlatform.enabled or WindowsPlatform.initialized:
|
|
return
|
|
|
|
if lib_pack := utils.env_to_path("FREECAD_LIBPACK_BIN"):
|
|
os.add_dll_directory(str(lib_pack.resolve()))
|
|
if win_dir := utils.env_to_path("WINDIR"):
|
|
system32 = win_dir / "system32"
|
|
os.add_dll_directory(str(system32.resolve()))
|
|
|
|
WindowsPlatform.initialized = True
|
|
|
|
def __bool__(self) -> bool:
|
|
return self.enabled
|
|
|
|
def add_dll_search_paths(self, paths: list[Path]) -> None:
|
|
for path in paths:
|
|
os.add_dll_directory(str(path.resolve()))
|
|
|
|
|
|
@transient
|
|
class DarwinPlatform:
|
|
"""
|
|
MacOSX specific hooks.
|
|
"""
|
|
|
|
enabled = platform.system() == "Darwin" and len(platform.mac_ver()[0]) > 0
|
|
|
|
def __bool__(self) -> bool:
|
|
return self.enabled
|
|
|
|
def post(self) -> None:
|
|
# add special path for MacOSX (bug #0000307): Where is this bug documented?
|
|
sys.path.append(os.path.expanduser("~/Library/Application Support/FreeCAD/Mod"))
|
|
|
|
|
|
class ModState(IntEnum):
|
|
Unsupported = -3
|
|
Failed = -2
|
|
Disabled = -1
|
|
Discovered = 0
|
|
Resolved = 1
|
|
Loaded = 2
|
|
|
|
|
|
@transient
|
|
class Mod:
|
|
"""
|
|
Base Mod.
|
|
|
|
There are two types of Mods: Directory based (DirMod) or Module based (ExtMod).
|
|
"""
|
|
|
|
ALL_ADDONS_DISABLED = "ALL_ADDONS_DISABLED"
|
|
ADDON_DISABLED = "ADDON_DISABLED"
|
|
PACKAGE_XML = "package.xml"
|
|
|
|
state: ModState
|
|
|
|
@property
|
|
def kind(self) -> str:
|
|
"""Return the Mod type: 'Dir' or 'Ext'"""
|
|
return "Ext"
|
|
|
|
@property
|
|
def init_mode(self) -> str:
|
|
"""Return the Mod init mode: 'exec' or 'import' or ''"""
|
|
return "import"
|
|
|
|
@property
|
|
def metadata(self) -> App.Metadata | None:
|
|
"""Return Metadata from package.xml if any."""
|
|
|
|
def check_disabled(self) -> bool:
|
|
"""
|
|
Mods can be disabled by several methods:
|
|
- command line argument: --disable-addon <name>
|
|
- stop file: ALL_ADDONS_DISABLED
|
|
- stop file: ADDON_DISABLED
|
|
"""
|
|
|
|
def process_metadata(self, search_paths: SearchPaths) -> None:
|
|
"""
|
|
Process package.xml if present to check version compatibility and to scan internal workbenches.
|
|
"""
|
|
|
|
def run_init(self) -> None:
|
|
"""
|
|
Run all required initialization scripts/modules: Init.py, init, __init__.py
|
|
"""
|
|
|
|
def supports_freecad_version(self) -> bool:
|
|
"""
|
|
Check if the Mod supports the current FreeCAD version.
|
|
"""
|
|
if meta := self.metadata:
|
|
return meta.supportsCurrentFreeCAD()
|
|
return True
|
|
|
|
def load(self, search_paths: SearchPaths) -> None:
|
|
"""
|
|
Load the Mod.
|
|
"""
|
|
try:
|
|
self.process_metadata(search_paths)
|
|
except Exception as ex:
|
|
self.state = ModState.Failed
|
|
Err(str(ex))
|
|
else:
|
|
if self.state == ModState.Resolved:
|
|
self.run_init()
|
|
if self.state == ModState.Resolved:
|
|
self.state = ModState.Loaded
|
|
|
|
|
|
@transient
|
|
class ExtMod(Mod):
|
|
"""
|
|
Module based Mod (aka extension module).
|
|
|
|
This kind of Mods are loaded using python module system, no direct filesystem or
|
|
compile/execute hacks are used.
|
|
|
|
extension modules must be defined in namespace freecad.*, i.e. freecad.MyAddon.
|
|
"""
|
|
|
|
name: str # full module name, i.e.: freecad.MyAddon
|
|
|
|
def __init__(self, name: str):
|
|
self.state = ModState.Resolved
|
|
self.name = name
|
|
|
|
@functools.cached_property
|
|
def metadata(self) -> App.Metadata | None:
|
|
with resources.as_file(resources.files(self.name)) as base:
|
|
metadata = base / self.PACKAGE_XML
|
|
if metadata.exists():
|
|
return App.Metadata(str(metadata))
|
|
metadata = base.parent.parent / self.PACKAGE_XML
|
|
if metadata.exists():
|
|
return App.Metadata(str(metadata))
|
|
return None
|
|
|
|
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()
|
|
|
|
def process_metadata(self, _search_paths: SearchPaths) -> None:
|
|
meta = self.metadata
|
|
if not meta:
|
|
return
|
|
|
|
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")
|
|
|
|
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')
|
|
Err(utils.HLine)
|
|
Log(error_msg)
|
|
Err(utils.HLine)
|
|
|
|
def run_init(self) -> None:
|
|
try:
|
|
module = importlib.import_module(self.name) # Implicit run of __init__.py
|
|
except Exception as ex:
|
|
self._init_error(ex, traceback.format_exc())
|
|
self.state = ModState.Failed
|
|
else:
|
|
self.run_secondary_init(module)
|
|
|
|
def run_secondary_init(self, module: types.ModuleType) -> None:
|
|
try:
|
|
importlib.import_module(f"{module.__name__}.init")
|
|
except ModuleNotFoundError:
|
|
pass # Ok, this module is optional
|
|
except Exception as ex:
|
|
self._init_error(ex, traceback.format_exc())
|
|
self.state = ModState.Failed
|
|
|
|
|
|
@transient
|
|
class DirMod(Mod):
|
|
"""
|
|
Directory based Mod. (aka Standard/Legacy).
|
|
|
|
This kind of Mods are scanned from several directories in the system
|
|
following certain priority. The name part of the path is used
|
|
as the module name.
|
|
|
|
Dir based modules can be overridden if several copies exists in different directories,
|
|
resolution is based on directory priority.
|
|
"""
|
|
|
|
INIT_PY = "Init.py"
|
|
|
|
_path: collections.deque[Path]
|
|
|
|
def __init__(self, path: Path) -> None:
|
|
self.state = ModState.Discovered
|
|
self._path = collections.deque()
|
|
self._path.append(path)
|
|
|
|
@property
|
|
def kind(self) -> str:
|
|
return "Dir"
|
|
|
|
@property
|
|
def init_mode(self) -> str:
|
|
return "exec" if (self.path / self.INIT_PY).exists() else ''
|
|
|
|
@property
|
|
def name(self) -> str:
|
|
return self.path.name
|
|
|
|
@functools.cached_property
|
|
def metadata(self) -> App.Metadata | None:
|
|
metadata = self.path / self.PACKAGE_XML
|
|
if metadata.exists():
|
|
return App.Metadata(str(metadata))
|
|
return None
|
|
|
|
def process_metadata(self, search_paths: SearchPaths):
|
|
meta = self.metadata
|
|
if not meta:
|
|
return
|
|
|
|
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")
|
|
return
|
|
|
|
content = meta.Content
|
|
if "workbench" in content:
|
|
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")
|
|
continue
|
|
|
|
subdirectory = workbench.Name if not workbench.Subdirectory else workbench.Subdirectory
|
|
subdirectory = re.split(r"[/\\]+", subdirectory)
|
|
subdirectory = self.path / Path(*subdirectory)
|
|
|
|
search_paths.add(
|
|
subdirectory,
|
|
env_path=PathPriority.OverrideLast,
|
|
sys_path=PathPriority.OverrideFirst,
|
|
dll_path=PathPriority.FallbackLast,
|
|
)
|
|
|
|
def override_with(self, path: Path) -> None:
|
|
"""
|
|
Override current path with the one provided.
|
|
"""
|
|
self._path.appendleft(path)
|
|
|
|
@property
|
|
def path(self) -> Path:
|
|
"""Current (highest priority) path."""
|
|
return self._path[0]
|
|
|
|
@property
|
|
def alternative_paths(self) -> list[Path]:
|
|
"""Alternative paths in priority order"""
|
|
return list(self._path)[1:]
|
|
|
|
def check_disabled(self) -> bool:
|
|
name = self.path.name
|
|
|
|
if name in Config.DisabledAddons:
|
|
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')
|
|
return True
|
|
|
|
return False
|
|
|
|
def resolve(self, search_paths: SearchPaths) -> None:
|
|
"""
|
|
Add the current path to search paths to make it loadable.
|
|
"""
|
|
if self.check_disabled():
|
|
self.state = ModState.Disabled
|
|
return
|
|
|
|
search_paths.add(
|
|
self.path,
|
|
env_path=PathPriority.OverrideLast,
|
|
sys_path=PathPriority.OverrideFirst,
|
|
)
|
|
|
|
self.state = ModState.Resolved
|
|
|
|
def run_init(self) -> None:
|
|
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")
|
|
return
|
|
|
|
try:
|
|
source = init_py.read_text(encoding="utf-8")
|
|
code = compile(source, init_py, 'exec')
|
|
exec(code)
|
|
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("Please look into the log file for further information")
|
|
self.state = ModState.Failed
|
|
else:
|
|
self.state = ModState.Loaded
|
|
Log(f"Init: Initializing {self.path!s}... done")
|
|
|
|
|
|
@transient
|
|
class ExtModScanner:
|
|
"""
|
|
Scan extension Mods from the python import path.
|
|
"""
|
|
|
|
mods: list[ExtMod]
|
|
|
|
def __init__(self):
|
|
self.mods = []
|
|
|
|
def scan(self):
|
|
import freecad
|
|
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')
|
|
continue
|
|
Log(f'Init: Initializing {module_name}')
|
|
|
|
def iter(self) -> coll_abc.Iterable[ExtMod]:
|
|
return self.mods
|
|
|
|
|
|
@transient
|
|
class DirModScanner:
|
|
"""
|
|
Sacan in the filesystem for Dir based Mods in the valid locations.
|
|
"""
|
|
|
|
EXCLUDE: set[str] = set(["", "CVS", "__init__.py"]) # Why?
|
|
mods: dict[str, DirMod]
|
|
visited: set[str]
|
|
|
|
def __init__(self) -> None:
|
|
self.mods = {}
|
|
self.visited = set()
|
|
|
|
def iter(self) -> coll_abc.Iterable[DirMod]:
|
|
"""All discovered Mods."""
|
|
return self.mods.values()
|
|
|
|
def dirs(self) -> list[Path]:
|
|
"""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:
|
|
"""
|
|
Scan in base with higher priority.
|
|
"""
|
|
if (key := str(base.resolve())) in self.visited:
|
|
return
|
|
|
|
self.visited.add(key)
|
|
|
|
if not base.exists():
|
|
if warning:
|
|
Wrn(warning)
|
|
else:
|
|
Wrn(f"No modules found in {base!s}")
|
|
return
|
|
|
|
if warning:
|
|
Wrn(warning)
|
|
|
|
if flat:
|
|
self.mods[str(base)] = DirMod(base)
|
|
return
|
|
|
|
for mod_dir in filter(Path.is_dir, base.iterdir()):
|
|
name = mod_dir.name.lower()
|
|
if name in DirModScanner.EXCLUDE:
|
|
continue
|
|
|
|
if mod := self.mods.get(name):
|
|
mod.override_with(mod_dir)
|
|
continue
|
|
|
|
self.mods[name] = DirMod(mod_dir)
|
|
|
|
|
|
# ┌────────────────────────────────────────────────┐
|
|
# │ Init Pipeline Definition │
|
|
# └────────────────────────────────────────────────┘
|
|
|
|
@transient
|
|
class InitPipeline:
|
|
"""
|
|
Init sequence, setup search paths, scan and load Mods and run platform specific hooks.
|
|
"""
|
|
|
|
std_home = Path(App.getHomePath()).resolve()
|
|
std_lib = Path(App.getLibraryDir()).resolve()
|
|
user_home = Path(App.getUserAppDataDir()).resolve()
|
|
dir_mod_scanner = DirModScanner()
|
|
ext_mod_scanner = ExtModScanner()
|
|
search_paths = SearchPaths()
|
|
|
|
def added_python_packages(self) -> PathSet:
|
|
"""
|
|
Additional python packages installed by AddonManager/pip.
|
|
"""
|
|
major, minor, _ = platform.python_version_tuple()
|
|
packages = self.user_home / "AdditionalPythonPackages"
|
|
vendor_path = packages / f"py{major}{minor}"
|
|
paths = PathSet()
|
|
paths.add(vendor_path)
|
|
paths.add(packages)
|
|
return paths
|
|
|
|
def scan(self) -> None:
|
|
"""
|
|
Scan step, search for standard directories, libraries and Mods.
|
|
"""
|
|
std_home = self.std_home
|
|
user_home = self.user_home
|
|
std_lib = self.std_lib
|
|
std_mod = std_home / "Mod"
|
|
std_ext = std_home / "Ext"
|
|
std_bin = std_home / "bin"
|
|
user_macro = Path(App.getUserMacroDir(True)).resolve()
|
|
user_mod = user_home / "Mod"
|
|
search_paths = self.search_paths
|
|
|
|
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")
|
|
|
|
# Libraries
|
|
libraries = PathSet()
|
|
libraries.add(std_home / "lib")
|
|
libraries.add(std_home / "lib64")
|
|
libraries.add(std_home / "lib-py3")
|
|
libraries.add(std_lib)
|
|
|
|
search_paths.add(
|
|
libraries,
|
|
env_path=PathPriority.Ignore,
|
|
sys_path=PathPriority.OverrideLast,
|
|
)
|
|
|
|
# Tools
|
|
search_paths.add(
|
|
std_home / "Tools",
|
|
env_path=PathPriority.Ignore,
|
|
sys_path=PathPriority.FallbackLast,
|
|
)
|
|
|
|
# Binaries
|
|
search_paths.add(
|
|
std_bin,
|
|
env_path=PathPriority.OverrideFirst,
|
|
sys_path=PathPriority.Ignore,
|
|
dll_path=PathPriority.FallbackLast,
|
|
)
|
|
|
|
# Scan for Directory based Mods
|
|
# Order is important because of overrides
|
|
Log("Init: Searching for modules...")
|
|
mods = self.dir_mod_scanner
|
|
mods.scan_and_override(std_mod)
|
|
mods.scan_and_override(user_mod)
|
|
mods.scan_and_override(user_macro / "Mod")
|
|
additional_mods = Config.AdditionalModulePaths + Config.AdditionalMacroPaths
|
|
for add in additional_mods:
|
|
mods.scan_and_override(add, flat=True)
|
|
|
|
# to have all the module-paths available in FreeCADGuiInit.py:
|
|
App.__ModDirs__ = [str(d) for d in mods.dirs()]
|
|
|
|
# this allows importing with:
|
|
# from FreeCAD.Module import package
|
|
import_path = PathSet([libraries])
|
|
import_path.add(std_mod, PathPriority.OverrideFirst)
|
|
import_path.add(user_mod, PathPriority.FallbackLast)
|
|
App.__path__ = [str(path) for path in import_path.build()]
|
|
|
|
# also add these directories to the sys.path to
|
|
# not change the old behavior. once we have moved to
|
|
# proper python modules this can eventually be removed.
|
|
search_paths.add(
|
|
std_mod,
|
|
env_path=PathPriority.Ignore,
|
|
sys_path=PathPriority.OverrideFirst,
|
|
)
|
|
search_paths.add(
|
|
std_ext,
|
|
env_path=PathPriority.Ignore,
|
|
sys_path=PathPriority.OverrideLast,
|
|
)
|
|
|
|
# Additional installed packages (AddonManager/pip)
|
|
search_paths.add(
|
|
self.added_python_packages(),
|
|
env_path=PathPriority.Ignore,
|
|
sys_path=PathPriority.FallbackLast,
|
|
)
|
|
|
|
# Resolve Dir Mods
|
|
for mod in mods.iter():
|
|
mod.resolve(search_paths)
|
|
|
|
def load_mods(self) -> None:
|
|
"""
|
|
Load Mods step, load both Dir based and Module based Mods.
|
|
"""
|
|
module_cache = []
|
|
|
|
# Update search paths to make Mods visible to import system.
|
|
search_paths = self.search_paths
|
|
search_paths.commit()
|
|
|
|
# Dir Mods first
|
|
for mod in self.dir_mod_scanner.iter():
|
|
if mod.state == ModState.Resolved:
|
|
mod.load(search_paths)
|
|
module_cache.append(mod)
|
|
|
|
# Update search paths: may have changed by dir loads
|
|
search_paths.commit()
|
|
|
|
# Finally, Module based Mod are loaded from python path
|
|
self.ext_mod_scanner.scan()
|
|
for mod in self.ext_mod_scanner.iter():
|
|
if mod.state == ModState.Resolved:
|
|
mod.load(search_paths)
|
|
module_cache.append(mod)
|
|
|
|
# Save to use in FreeCADGuiInit.py
|
|
App.__ModCache__ = module_cache
|
|
|
|
def register_macro_sources(self) -> None:
|
|
"""
|
|
Add Macro sources to search paths.
|
|
"""
|
|
std_macro = self.std_home / "Macro"
|
|
user_macro_default = Path(App.getUserMacroDir(False)).resolve()
|
|
user_macro = Path(App.getUserMacroDir(True)).resolve()
|
|
|
|
# add MacroDir to path (RFE #0000504)
|
|
self.search_paths.add(
|
|
user_macro_default,
|
|
env_path=PathPriority.Ignore,
|
|
sys_path=PathPriority.FallbackLast,
|
|
)
|
|
self.search_paths.add(
|
|
user_macro,
|
|
env_path=PathPriority.Ignore,
|
|
sys_path=PathPriority.FallbackLast,
|
|
)
|
|
self.search_paths.add(
|
|
std_macro,
|
|
env_path=PathPriority.Ignore,
|
|
sys_path=PathPriority.FallbackLast,
|
|
)
|
|
|
|
def post(self) -> None:
|
|
"""
|
|
Run final steps.
|
|
"""
|
|
if macosx := DarwinPlatform():
|
|
macosx.post()
|
|
|
|
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():
|
|
utils.setup_tty_tab_completion()
|
|
|
|
def report(self) -> None:
|
|
std_mod = self.std_home / "Mod"
|
|
Log(f"Using {std_mod!s} as module path!")
|
|
|
|
Log("System path after init:")
|
|
for path in os.environ["PATH"].split(os.pathsep):
|
|
Log(f" {path}")
|
|
|
|
Log("FreeCADInit Mod summary:")
|
|
output = []
|
|
output.append(f"+-{'--':-<24}-+-{'----':-<10}-+-{'---':-<6}-+-{'-----':-<48}-")
|
|
output.append(f"| {'Mod':<24} | {'State':<10} | {'Mode':<6} | {'Source':<48} ")
|
|
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}")
|
|
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}")
|
|
|
|
for line in output:
|
|
Log(line)
|
|
Log(output[0])
|
|
|
|
def run(self) -> None:
|
|
"""
|
|
Pipeline entry point.
|
|
"""
|
|
self.scan()
|
|
self.load_mods()
|
|
self.register_macro_sources()
|
|
self.post()
|
|
self.report()
|
|
self.setup_tty()
|
|
|
|
|
|
# ┌────────────────────────────────────────────────┐
|
|
# │ Init Applications │
|
|
# └────────────────────────────────────────────────┘
|
|
|
|
@transient
|
|
@call_in_place
|
|
def init_applications() -> None:
|
|
try:
|
|
InitPipeline().run()
|
|
Log('Init: App::FreeCADInit.py done')
|
|
except Exception as ex:
|
|
Err(f'Error in init_applications {ex!s}')
|
|
Err(utils.HLine)
|
|
Err(traceback.format_exc())
|
|
Err(utils.HLine)
|
|
|
|
|
|
# ┌────────────────────────────────────────────────┐
|
|
# │ Cleanup for next scripts │
|
|
# └────────────────────────────────────────────────┘
|
|
|
|
# Reset logger to no extra newline for subsequent scripts (Backwards compat)
|
|
__logger.sep = ""
|
|
|
|
# Clean global namespace
|
|
transient.cleanup()
|