Files
Web_FreeCAD_Bitbybit/scripts/freecad-reference-probe.py

265 lines
9.8 KiB
Python

import importlib
import importlib.util
import json
import os
import sys
import FreeCAD as App
def property_metadata(obj):
def json_value(value):
if value is None or isinstance(value, (bool, int, float, str)):
return value
if isinstance(value, (list, tuple)):
return [json_value(item) for item in value]
if isinstance(value, dict):
return {str(key): json_value(item) for key, item in value.items()}
# FreeCAD quantities and placements keep a stable textual form while
# the report remains valid JSON for every registered property type.
try:
return str(value)
except Exception:
return "<unserializable>"
properties = []
for name in obj.PropertiesList:
try:
type_id = obj.getTypeIdOfProperty(name)
except Exception:
type_id = "unknown"
try:
group = obj.getGroupOfProperty(name)
except Exception:
group = ""
try:
status = list(obj.getPropertyStatus(name))
except Exception:
status = []
try:
default = json_value(getattr(obj, name))
except Exception:
default = "<unreadable>"
properties.append({"name": name, "typeId": type_id, "group": group, "status": status, "default": default})
return properties
def probe_object(document, type_id):
object_name = "Reference" + type_id.replace(":", "_")
try:
obj = document.addObject(type_id, object_name)
except Exception as error:
return {"typeId": type_id, "available": False, "probeStatus": "unavailable", "error": str(error)}
return {
"typeId": type_id,
"available": True,
"probeStatus": "available",
"name": obj.Name,
"runtimeTypeId": obj.TypeId,
"properties": property_metadata(obj),
}
def probe_registered_objects(document):
try:
registered = sorted(set(str(type_id) for type_id in document.supportedTypes()))
except Exception as error:
return {"available": False, "types": [], "error": str(error)}
records = []
for index, type_id in enumerate(registered):
record = probe_object(document, type_id)
record["candidateIndex"] = index
record["candidateSource"] = "Document.supportedTypes"
records.append(record)
# A few GUI-only TechDraw providers dereference their view during
# removeObject(). Keep those instances alive until FreeCAD exits so a
# probe cannot turn a valid metadata observation into a process crash.
if not bool(getattr(App, "GuiUp", False)) and record.get("available") and record.get("name"):
try:
document.removeObject(record["name"])
except Exception as error:
record["cleanupError"] = str(error)
return {
"available": True,
"candidateSource": "Document.supportedTypes",
"candidateCount": len(records),
"availableCount": sum(1 for record in records if record.get("available")),
"unavailableCount": sum(1 for record in records if not record.get("available")),
"types": records,
}
ALL_REFERENCE_MODULES = [
"AddonManager", "Assembly", "BIM", "CAM", "Cloud", "Draft", "Fem", "Help",
"Idf", "Import", "Inspection", "JtReader", "Material", "Measure", "Mesh",
"MeshPart", "OpenSCAD", "Part", "PartDesign", "Plot", "Points",
"ReverseEngineering", "Robot", "Sandbox", "Show", "Sketcher", "Spreadsheet",
"Start", "Surface", "TechDraw", "TemplatePyMod", "Test", "Tux", "Web",
]
GUI_REQUIRED_MODULES = {
"AddonManager", "Assembly", "BIM", "CAM", "Draft", "Fem", "Help", "Idf",
"Import", "Inspection", "JtReader", "Material", "Measure", "Mesh", "MeshPart",
"OpenSCAD", "Part", "PartDesign", "Plot", "Points", "ReverseEngineering", "Robot",
"Sandbox", "Show", "Sketcher", "Spreadsheet", "Start", "Surface", "TechDraw", "Tux",
"Web",
}
def module_is_installed(name):
suffixes = ("", ".py", ".so", ".pyd", ".dll", ".dylib")
roots = [path for path in sys.path if path]
try:
resource_dir = App.getResourceDir()
roots.extend([resource_dir, os.path.join(resource_dir, "Mod")])
except Exception:
pass
for root in roots:
for suffix in suffixes:
if os.path.exists(os.path.join(root, name + suffix)):
return True
if os.path.exists(os.path.join(root, name, "__init__" + suffix)):
return True
if os.path.exists(os.path.join(root, "Mod", name + suffix)):
return True
return False
def probe_module(name):
gui_required = name in GUI_REQUIRED_MODULES
installed = module_is_installed(name)
spec = None
try:
spec = importlib.util.find_spec(name)
except Exception:
spec = None
try:
module = importlib.import_module(name)
importable = True
import_error = None
file = getattr(module, "__file__", None)
except Exception as caught_error:
importable = False
import_error = caught_error
file = getattr(spec, "origin", None) if spec else None
compiled = bool(installed or spec)
if importable and gui_required and not bool(getattr(App, "GuiUp", False)):
runtime_status = "gui-only-unprobeable"
elif importable:
runtime_status = "compiled-importable"
elif compiled:
runtime_status = "compiled-import-failure"
else:
runtime_status = "not-built"
result = {
"name": name,
"available": importable,
"compiled": compiled,
"importable": importable,
"guiRequired": gui_required,
"guiAvailable": bool(getattr(App, "GuiUp", False)),
"runtimeStatus": runtime_status,
"file": file,
}
if import_error is not None:
result["error"] = str(import_error)
return result
def probe_gui_commands(document, selected_object):
if not bool(getattr(App, "GuiUp", False)):
return {"available": False, "workbenches": [], "commands": []}
try:
import FreeCADGui as Gui
except Exception as error:
return {"available": False, "workbenches": [], "commands": [], "error": str(error)}
try:
workbench_names = sorted(Gui.listWorkbenches().keys())
except Exception:
workbench_names = []
command_observations = {}
def action_enabled(command_id):
try:
command = Gui.Command.get(command_id)
if command is None:
return None
actions = command.getAction()
if not actions:
return None
return bool(actions[0].isEnabled())
except Exception as error:
return {"error": str(error)}
workbenches = []
for workbench_name in workbench_names:
try:
Gui.activateWorkbench(workbench_name)
command_ids = sorted(set(Gui.listCommands()))
Gui.Selection.clearSelection()
empty_selection = {command_id: action_enabled(command_id) for command_id in command_ids}
Gui.Selection.addSelection(document.Name, selected_object.Name)
selected_selection = {command_id: action_enabled(command_id) for command_id in command_ids}
Gui.Selection.clearSelection()
observations = {
command_id: {
"emptySelection": empty_selection.get(command_id),
"selectedPartBox": selected_selection.get(command_id),
}
for command_id in command_ids
}
for command_id, observation in observations.items():
command_observations.setdefault(command_id, []).append({"workbench": workbench_name, **observation})
workbenches.append({"name": workbench_name, "commandCount": len(command_ids), "status": "probed"})
except Exception as error:
try:
Gui.Selection.clearSelection()
except Exception:
pass
workbenches.append({"name": workbench_name, "commandCount": 0, "status": "probe-failure", "error": str(error)})
return {
"available": True,
"workbenches": workbenches,
"commands": [
{"id": command_id, "observations": observations}
for command_id, observations in sorted(command_observations.items())
],
}
document = App.newDocument("ReferenceProbe")
box_object = document.addObject("Part::Box", "CommandProbeBox")
modules = [probe_module(name) for name in ALL_REFERENCE_MODULES]
gui_commands = probe_gui_commands(document, box_object)
runtime_objects = probe_registered_objects(document)
version = App.Version()
result = {
"schemaVersion": 1,
"baselineId": "freecad-1.1.1",
"freecadVersion": ".".join(str(value) for value in version[:3]),
"revision": str(version[3]),
"gitBranch": str(version[6]) if len(version) > 6 else "",
"gitCommit": str(version[7]) if len(version) > 7 else "",
"guiUp": bool(getattr(App, "GuiUp", False)),
"buildPurpose": "desktop-reference-oracle" if bool(getattr(App, "GuiUp", False)) else "headless-reference-oracle",
"moduleCount": len(ALL_REFERENCE_MODULES),
"modules": modules,
"objects": [probe_object(document, type_id) for type_id in [
"Part::Box", "Part::Cylinder", "Part::Sphere", "Part::Cone",
"Part::Feature", "PartDesign::Feature", "Sketcher::SketchObject",
]],
"runtimeObjects": runtime_objects,
"guiCommands": gui_commands,
}
result["moduleStatusSummary"] = {
status: sum(1 for module in result["modules"] if module["runtimeStatus"] == status)
for status in sorted({module["runtimeStatus"] for module in result["modules"]})
}
App.closeDocument(document.Name)
print("FREECAD_REFERENCE_RESULT=" + json.dumps(result, sort_keys=True, separators=(",", ":")))
sys.stdout.flush()
sys.exit(0)