diff --git a/src/Mod/AddonManager/AddonManagerTest/app/test_freecad_interface.py b/src/Mod/AddonManager/AddonManagerTest/app/test_freecad_interface.py
new file mode 100644
index 0000000000..3630314d41
--- /dev/null
+++ b/src/Mod/AddonManager/AddonManagerTest/app/test_freecad_interface.py
@@ -0,0 +1,306 @@
+# ***************************************************************************
+# * *
+# * Copyright (c) 2023 FreeCAD Project Association *
+# * *
+# * This file is part of FreeCAD. *
+# * *
+# * FreeCAD is free software: you can redistribute it and/or modify it *
+# * under the terms of the GNU Lesser General Public License as *
+# * published by the Free Software Foundation, either version 2.1 of the *
+# * License, or (at your option) any later version. *
+# * *
+# * 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 Lesser General Public *
+# * License along with FreeCAD. If not, see *
+# * . *
+# * *
+# ***************************************************************************
+
+"""Tests for the Addon Manager's FreeCAD interface classes."""
+
+import json
+import os
+import sys
+import tempfile
+import unittest
+from unittest.mock import patch, MagicMock
+
+# pylint: disable=protected-access,import-outside-toplevel
+
+
+class TestConsole(unittest.TestCase):
+ """Tests for the Console"""
+
+ def setUp(self) -> None:
+ self.saved_freecad = None
+ if "FreeCAD" in sys.modules:
+ self.saved_freecad = sys.modules["FreeCAD"]
+ sys.modules.pop("FreeCAD")
+ if "addonmanager_freecad_interface" in sys.modules:
+ sys.modules.pop("addonmanager_freecad_interface")
+ sys.path.append("../../")
+
+ def tearDown(self) -> None:
+ if "FreeCAD" in sys.modules:
+ sys.modules.pop("FreeCAD")
+ if self.saved_freecad is not None:
+ sys.modules["FreeCAD"] = self.saved_freecad
+
+ def test_log_with_freecad(self):
+ """Ensure that if FreeCAD exists, the appropriate function is called"""
+ sys.modules["FreeCAD"] = unittest.mock.MagicMock()
+ import addonmanager_freecad_interface as fc
+
+ fc.Console.PrintLog("Test output")
+ self.assertTrue(isinstance(fc.Console, unittest.mock.MagicMock))
+ self.assertTrue(fc.Console.PrintLog.called)
+
+ def test_log_no_freecad(self):
+ """Test that if the FreeCAD import fails, the logger is set up correctly, and
+ implements PrintLog"""
+ sys.modules["FreeCAD"] = None
+ with patch(
+ "addonmanager_freecad_interface.logging", new=MagicMock()
+ ) as mock_logging:
+ import addonmanager_freecad_interface as fc
+
+ fc.Console.PrintLog("Test output")
+ self.assertTrue(isinstance(fc.Console, fc.ConsoleReplacement))
+ self.assertTrue(mock_logging.log.called)
+
+ def test_message_no_freecad(self):
+ """Test that if the FreeCAD import fails the logger implements PrintMessage"""
+ sys.modules["FreeCAD"] = None
+ with patch(
+ "addonmanager_freecad_interface.logging", new=MagicMock()
+ ) as mock_logging:
+ import addonmanager_freecad_interface as fc
+
+ fc.Console.PrintMessage("Test output")
+ self.assertTrue(mock_logging.info.called)
+
+ def test_warning_no_freecad(self):
+ """Test that if the FreeCAD import fails the logger implements PrintWarning"""
+ sys.modules["FreeCAD"] = None
+ with patch(
+ "addonmanager_freecad_interface.logging", new=MagicMock()
+ ) as mock_logging:
+ import addonmanager_freecad_interface as fc
+
+ fc.Console.PrintWarning("Test output")
+ self.assertTrue(mock_logging.warning.called)
+
+ def test_error_no_freecad(self):
+ """Test that if the FreeCAD import fails the logger implements PrintError"""
+ sys.modules["FreeCAD"] = None
+ with patch(
+ "addonmanager_freecad_interface.logging", new=MagicMock()
+ ) as mock_logging:
+ import addonmanager_freecad_interface as fc
+
+ fc.Console.PrintError("Test output")
+ self.assertTrue(mock_logging.error.called)
+
+
+class TestParameters(unittest.TestCase):
+ """Tests for the Parameters"""
+
+ def setUp(self) -> None:
+ self.saved_freecad = None
+ if "FreeCAD" in sys.modules:
+ self.saved_freecad = sys.modules["FreeCAD"]
+ sys.modules.pop("FreeCAD")
+ if "addonmanager_freecad_interface" in sys.modules:
+ sys.modules.pop("addonmanager_freecad_interface")
+ sys.path.append("../../")
+
+ def tearDown(self) -> None:
+ if "FreeCAD" in sys.modules:
+ sys.modules.pop("FreeCAD")
+ if self.saved_freecad is not None:
+ sys.modules["FreeCAD"] = self.saved_freecad
+
+ def test_param_get_with_freecad(self):
+ """Ensure that if FreeCAD exists, the built-in FreeCAD function is called"""
+ sys.modules["FreeCAD"] = unittest.mock.MagicMock()
+ import addonmanager_freecad_interface as fc
+
+ prefs = fc.ParamGet("some/fake/path")
+ self.assertTrue(isinstance(prefs, unittest.mock.MagicMock))
+
+ def test_param_get_no_freecad(self):
+ """Test that if the FreeCAD import fails, param_get returns a ParametersReplacement"""
+ sys.modules["FreeCAD"] = None
+ import addonmanager_freecad_interface as fc
+
+ prefs = fc.ParamGet("some/fake/path")
+ self.assertTrue(isinstance(prefs, fc.ParametersReplacement))
+
+ def test_replacement_getters_and_setters(self):
+ """Test that ParameterReplacement's getters, setters, and deleters work"""
+ sys.modules["FreeCAD"] = None
+ import addonmanager_freecad_interface as fc
+
+ prf = fc.ParamGet("some/fake/path")
+ gs_types = [
+ ("Bool", prf.GetBool, prf.SetBool, prf.RemBool, True, False),
+ ("Int", prf.GetInt, prf.SetInt, prf.RemInt, 42, 0),
+ ("Float", prf.GetFloat, prf.SetFloat, prf.RemFloat, 1.2, 3.4),
+ ("String", prf.GetString, prf.SetString, prf.RemString, "test", "other"),
+ ]
+ for gs_type in gs_types:
+ with self.subTest(msg=f"Testing {gs_type[0]}", gs_type=gs_type):
+ getter = gs_type[1]
+ setter = gs_type[2]
+ deleter = gs_type[3]
+ value_1 = gs_type[4]
+ value_2 = gs_type[5]
+ self.assertEqual(getter("test", value_1), value_1)
+ self.assertEqual(getter("test", value_2), value_2)
+ self.assertNotIn("test", prf.parameters)
+ setter("test", value_1)
+ self.assertIn("test", prf.parameters)
+ self.assertEqual(getter("test", value_2), value_1)
+ deleter("test")
+ self.assertNotIn("test", prf.parameters)
+
+
+class TestDataPaths(unittest.TestCase):
+ """Tests for the data paths"""
+
+ def setUp(self) -> None:
+ self.saved_freecad = None
+ if "FreeCAD" in sys.modules:
+ self.saved_freecad = sys.modules["FreeCAD"]
+ sys.modules.pop("FreeCAD")
+ if "addonmanager_freecad_interface" in sys.modules:
+ sys.modules.pop("addonmanager_freecad_interface")
+ sys.path.append("../../")
+
+ def tearDown(self) -> None:
+ if "FreeCAD" in sys.modules:
+ sys.modules.pop("FreeCAD")
+ if self.saved_freecad is not None:
+ sys.modules["FreeCAD"] = self.saved_freecad
+
+ def test_init_with_freecad(self):
+ """Ensure that if FreeCAD exists, the appropriate functions are called"""
+ sys.modules["FreeCAD"] = unittest.mock.MagicMock()
+ import addonmanager_freecad_interface as fc
+
+ data_paths = fc.DataPaths()
+ self.assertTrue(sys.modules["FreeCAD"].getUserAppDataDir.called)
+ self.assertTrue(sys.modules["FreeCAD"].getUserMacroDir.called)
+ self.assertTrue(sys.modules["FreeCAD"].getUserCachePath.called)
+ self.assertIsNotNone(data_paths.mod_dir)
+ self.assertIsNotNone(data_paths.cache_dir)
+ self.assertIsNotNone(data_paths.macro_dir)
+
+ def test_init_without_freecad(self):
+ """Ensure that if FreeCAD does not exist, the appropriate functions are called"""
+ sys.modules["FreeCAD"] = None
+ import addonmanager_freecad_interface as fc
+
+ data_paths = fc.DataPaths()
+ self.assertIsNotNone(data_paths.mod_dir)
+ self.assertIsNotNone(data_paths.cache_dir)
+ self.assertIsNotNone(data_paths.macro_dir)
+ self.assertNotEqual(data_paths.mod_dir, data_paths.cache_dir)
+ self.assertNotEqual(data_paths.mod_dir, data_paths.macro_dir)
+ self.assertNotEqual(data_paths.cache_dir, data_paths.macro_dir)
+
+
+class TestPreferences(unittest.TestCase):
+ """Tests for the preferences wrapper"""
+
+ def setUp(self) -> None:
+ sys.path.append("../../")
+ import addonmanager_freecad_interface as fc
+
+ self.fc = fc
+
+ def tearDown(self) -> None:
+ pass
+
+ def test_load_preferences_defaults(self):
+ """Preferences are loaded from a given file"""
+ defaults = self.given_defaults()
+ with tempfile.TemporaryDirectory() as temp_dir:
+ json_file = os.path.join(temp_dir, "defaults.json")
+ with open(json_file, "w", encoding="utf-8") as f:
+ f.write(json.dumps(defaults))
+ self.fc.Preferences._load_preferences_defaults(json_file)
+ self.assertDictEqual(defaults, self.fc.Preferences.preferences_defaults)
+
+ def test_in_memory_defaults(self):
+ """Preferences are loaded from memory"""
+ defaults = self.given_defaults()
+ prefs = self.fc.Preferences(defaults)
+ self.assertDictEqual(defaults, prefs.preferences_defaults)
+
+ def test_get_good(self):
+ """Get returns results when matching an existing preference"""
+ defaults = self.given_defaults()
+ prefs = self.fc.Preferences(defaults)
+ self.assertEqual(prefs.get("TestBool"), defaults["TestBool"])
+ self.assertEqual(prefs.get("TestInt"), defaults["TestInt"])
+ self.assertEqual(prefs.get("TestFloat"), defaults["TestFloat"])
+ self.assertEqual(prefs.get("TestString"), defaults["TestString"])
+
+ def test_get_nonexistent(self):
+ """Get raises an exception when asked for a non-existent preference"""
+ defaults = self.given_defaults()
+ prefs = self.fc.Preferences(defaults)
+ with self.assertRaises(RuntimeError):
+ prefs.get("No_such_thing")
+
+ def test_get_bad_type(self):
+ """Get raises an exception when getting an unsupported type"""
+ defaults = self.given_defaults()
+ defaults["TestArray"] = ["This", "Is", "Legal", "JSON"]
+ prefs = self.fc.Preferences(defaults)
+ with self.assertRaises(RuntimeError):
+ prefs.get("TestArray")
+
+ def test_set_good(self):
+ """Set works when matching an existing preference"""
+ defaults = self.given_defaults()
+ prefs = self.fc.Preferences(defaults)
+ prefs.set("TestBool", False)
+ self.assertEqual(prefs.get("TestBool"), False)
+ prefs.set("TestInt", 4321)
+ self.assertEqual(prefs.get("TestInt"), 4321)
+ prefs.set("TestFloat", 3.14159)
+ self.assertEqual(prefs.get("TestFloat"), 3.14159)
+ prefs.set("TestString", "Forty two")
+ self.assertEqual(prefs.get("TestString"), "Forty two")
+
+ def test_set_nonexistent(self):
+ """Set raises an exception when asked for a non-existent preference"""
+ defaults = self.given_defaults()
+ prefs = self.fc.Preferences(defaults)
+ with self.assertRaises(RuntimeError):
+ prefs.get("No_such_thing")
+
+ def test_set_bad_type(self):
+ """Set raises an exception when setting an unsupported type"""
+ defaults = self.given_defaults()
+ defaults["TestArray"] = ["This", "Is", "Legal", "JSON"]
+ prefs = self.fc.Preferences(defaults)
+ with self.assertRaises(RuntimeError):
+ prefs.get("TestArray")
+
+ @staticmethod
+ def given_defaults():
+ """Get a dictionary of fake defaults for testing"""
+ defaults = {
+ "TestBool": True,
+ "TestInt": 42,
+ "TestFloat": 1.2,
+ "TestString": "Test",
+ }
+ return defaults
diff --git a/src/Mod/AddonManager/CMakeLists.txt b/src/Mod/AddonManager/CMakeLists.txt
index dad3d3d455..d4c2b856e1 100644
--- a/src/Mod/AddonManager/CMakeLists.txt
+++ b/src/Mod/AddonManager/CMakeLists.txt
@@ -19,6 +19,7 @@ SET(AddonManager_SRCS
addonmanager_devmode_predictor.py
addonmanager_devmode_validators.py
addonmanager_firstrun.py
+ addonmanager_freecad_interface.py
addonmanager_git.py
addonmanager_installer.py
addonmanager_installer_gui.py
@@ -83,6 +84,7 @@ SET(AddonManagerTestsApp_SRCS
AddonManagerTest/app/mocks.py
AddonManagerTest/app/test_addon.py
AddonManagerTest/app/test_dependency_installer.py
+ AddonManagerTest/app/test_freecad_interface.py
AddonManagerTest/app/test_git.py
AddonManagerTest/app/test_installer.py
AddonManagerTest/app/test_macro.py
diff --git a/src/Mod/AddonManager/TestAddonManagerApp.py b/src/Mod/AddonManager/TestAddonManagerApp.py
index 49595584f5..cd460b15e4 100644
--- a/src/Mod/AddonManager/TestAddonManagerApp.py
+++ b/src/Mod/AddonManager/TestAddonManagerApp.py
@@ -44,14 +44,37 @@ from AddonManagerTest.app.test_uninstaller import (
TestAddonUninstaller as AddonManagerTestAddonUninstaller,
TestMacroUninstaller as AddonManagerTestMacroUninstaller,
)
+from AddonManagerTest.app.test_freecad_interface import (
+ TestConsole as AddonManagerTestConsole,
+ TestParameters as AddonManagerTestParameters,
+ TestDataPaths as AddonManagerTestDataPaths,
+)
-# dummy usage to get flake8 and lgtm quiet
-False if AddonManagerTestUtilities.__name__ else True
-False if AddonManagerTestAddon.__name__ else True
-False if AddonManagerTestMacro.__name__ else True
-False if AddonManagerTestGit.__name__ else True
-False if AddonManagerTestAddonInstaller.__name__ else True
-False if AddonManagerTestMacroInstaller.__name__ else True
-False if AddonManagerTestDependencyInstaller.__name__ else True
-False if AddonManagerTestAddonUninstaller.__name__ else True
-False if AddonManagerTestMacroUninstaller.__name__ else True
+
+class TestListTerminator:
+ pass
+
+
+# Basic usage mostly to get static analyzers to stop complaining about unused imports
+try:
+ import FreeCAD
+except ImportError:
+ FreeCAD = None
+loaded_gui_tests = [
+ AddonManagerTestUtilities,
+ AddonManagerTestAddon,
+ AddonManagerTestMacro,
+ AddonManagerTestGit,
+ AddonManagerTestAddonInstaller,
+ AddonManagerTestMacroInstaller,
+ AddonManagerTestDependencyInstaller,
+ AddonManagerTestAddonUninstaller,
+ AddonManagerTestMacroUninstaller,
+ AddonManagerTestConsole,
+ AddonManagerTestParameters,
+ AddonManagerTestDataPaths,
+ TestListTerminator # Needed to prevent the last test from running twice
+]
+if FreeCAD:
+ for test in loaded_gui_tests:
+ FreeCAD.Console.PrintLog(f"Loaded tests from {test.__name__}\n")
diff --git a/src/Mod/AddonManager/addonmanager_freecad_interface.py b/src/Mod/AddonManager/addonmanager_freecad_interface.py
new file mode 100644
index 0000000000..f0624078bd
--- /dev/null
+++ b/src/Mod/AddonManager/addonmanager_freecad_interface.py
@@ -0,0 +1,262 @@
+# ***************************************************************************
+# * *
+# * Copyright (c) 2023 FreeCAD Project Association *
+# * *
+# * This file is part of FreeCAD. *
+# * *
+# * FreeCAD is free software: you can redistribute it and/or modify it *
+# * under the terms of the GNU Lesser General Public License as *
+# * published by the Free Software Foundation, either version 2.1 of the *
+# * License, or (at your option) any later version. *
+# * *
+# * 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 Lesser General Public *
+# * License along with FreeCAD. If not, see *
+# * . *
+# * *
+# ***************************************************************************
+
+"""Classes to encapsulate the Addon Manager's interaction with FreeCAD, and to provide
+replacements when the Addon Manager is not run from within FreeCAD (e.g. during unit
+testing).
+
+Usage:
+from addonmanager_freecad_interface import Console, DataPaths, Preferences
+"""
+
+import json
+import logging
+import os
+import tempfile
+
+# pylint: disable=too-few-public-methods
+
+try:
+ import FreeCAD
+
+ Console = FreeCAD.Console
+ ParamGet = FreeCAD.ParamGet
+ getUserAppDataDir = FreeCAD.getUserAppDataDir
+ getUserMacroDir = FreeCAD.getUserMacroDir
+ getUserCachePath = FreeCAD.getUserCachePath
+ translate = FreeCAD.Qt.translate
+
+except ImportError:
+ FreeCAD = None
+ getUserAppDataDir = None
+ getUserCachePath = None
+ getUserMacroDir = None
+
+ def translate(_context: str, string: str, _desc: str = "") -> str:
+ return string
+
+ class ConsoleReplacement:
+ """If FreeCAD's Console is not available, create a replacement by redirecting FreeCAD
+ log calls to Python's built-in logging facility."""
+
+ @staticmethod
+ def PrintLog(arg: str) -> None:
+ logging.log(logging.DEBUG, arg)
+
+ @staticmethod
+ def PrintMessage(arg: str) -> None:
+ logging.info(arg)
+
+ @staticmethod
+ def PrintWarning(arg: str) -> None:
+ logging.warning(arg)
+
+ @staticmethod
+ def PrintError(arg: str) -> None:
+ logging.error(arg)
+
+ Console = ConsoleReplacement()
+
+ class ParametersReplacement:
+ """Proxy for FreeCAD's Parameters when not running within FreeCAD. NOT
+ serialized, only exists for the duration of the program's execution. Only
+ provides the functions used by the Addon Manager, this class is not intended
+ to be a complete replacement for FreeCAD's preferences system."""
+
+ parameters = {}
+
+ def GetBool(self, name: str, default: bool) -> bool:
+ return self._Get(name, default)
+
+ def GetInt(self, name: str, default: int) -> int:
+ return self._Get(name, default)
+
+ def GetFloat(self, name: str, default: float) -> float:
+ return self._Get(name, default)
+
+ def GetString(self, name: str, default: str) -> str:
+ return self._Get(name, default)
+
+ def _Get(self, name, default):
+ return self.parameters[name] if name in self.parameters else default
+
+ def SetBool(self, name: str, value: bool) -> None:
+ self.parameters[name] = value
+
+ def SetInt(self, name: str, value: int) -> None:
+ self.parameters[name] = value
+
+ def SetFloat(self, name: str, value: float) -> None:
+ self.parameters[name] = value
+
+ def SetString(self, name: str, value: str) -> None:
+ self.parameters[name] = value
+
+ def RemBool(self, name: str) -> None:
+ self.parameters.pop(name)
+
+ def RemInt(self, name: str) -> None:
+ self.parameters.pop(name)
+
+ def RemFloat(self, name: str) -> None:
+ self.parameters.pop(name)
+
+ def RemString(self, name: str) -> None:
+ self.parameters.pop(name)
+
+ def ParamGet(_: str):
+ return ParametersReplacement()
+
+
+class DataPaths:
+ """Provide access to various data storage paths. If not running within FreeCAD,
+ all paths are temp directories. If not run within FreeCAD, all directories are
+ deleted when the last reference to this class is deleted."""
+
+ mod_dir = None
+ macro_dir = None
+ cache_dir = None
+
+ reference_count = 0
+
+ def __init__(self):
+ if FreeCAD:
+ if self.mod_dir is None:
+ self.mod_dir = os.path.join(getUserAppDataDir(), "Mod")
+ if self.cache_dir is None:
+ self.cache_dir = getUserCachePath()
+ if self.macro_dir is None:
+ self.macro_dir = getUserMacroDir(True)
+ else:
+ self.reference_count += 1
+ if self.mod_dir is None:
+ self.mod_dir = tempfile.mkdtemp()
+ if self.cache_dir is None:
+ self.cache_dir = tempfile.mkdtemp()
+ if self.macro_dir is None:
+ self.macro_dir = tempfile.mkdtemp()
+
+ def __del__(self):
+ self.reference_count -= 1
+ if not FreeCAD and self.reference_count <= 0:
+ os.rmdir(self.mod_dir)
+ os.rmdir(self.cache_dir)
+ os.rmdir(self.macro_dir)
+ self.mod_dir = None
+ self.cache_dir = None
+ self.macro_dir = None
+
+
+class Preferences:
+ """Wrap access to all user preferences. If run within FreeCAD, user preferences are
+ persistent, otherwise they only exist per-run. All preferences are controlled by a
+ central JSON file defining their defaults."""
+
+ preferences_defaults = {}
+
+ def __init__(self, defaults_data=None):
+ """Set up the preferences, initializing the class statics if necessary. If
+ defaults_data is provided it is used as the preferences defaults. If it is not
+ provided, then the defaults are read in from the standard defaults file,
+ addonmanager_preferences_defaults.json, located in the same directory as this
+ Python file."""
+ if not self.preferences_defaults:
+ if defaults_data:
+ self.preferences_defaults = defaults_data
+ else:
+ self._load_preferences_defaults()
+ self.prefs = ParamGet("User parameter:BaseApp/Preferences/Addons")
+
+ def get(self, name: str):
+ """Get the preference value for the given key"""
+ if name not in self.preferences_defaults:
+ raise RuntimeError(
+ f"Unrecognized preference {name} -- did you add "
+ + "it to addonmanager_preferences_defaults.json?"
+ )
+ if isinstance(self.preferences_defaults[name], bool):
+ return self.prefs.GetBool(name, self.preferences_defaults[name])
+ if isinstance(self.preferences_defaults[name], int):
+ return self.prefs.GetInt(name, self.preferences_defaults[name])
+ if isinstance(self.preferences_defaults[name], float):
+ return self.prefs.GetFloat(name, self.preferences_defaults[name])
+ if isinstance(self.preferences_defaults[name], str):
+ return self.prefs.GetString(name, self.preferences_defaults[name])
+ # We don't directly support any other types from the JSON file (e.g. arrays)
+ type_name = type(self.preferences_defaults[name])
+ raise RuntimeError(f"Unrecognized type for {name}: {type_name}")
+
+ def set(self, name: str, value):
+ """Set the preference value for the given key. Must exist (e.g. must be in the
+ addonmanager_preferences_defaults.json file)."""
+ if name not in self.preferences_defaults:
+ raise RuntimeError(
+ f"Unrecognized preference {name} -- did you add "
+ + "it to addonmanager_preferences_defaults.json?"
+ )
+ if isinstance(self.preferences_defaults[name], bool):
+ self.prefs.SetBool(name, value)
+ elif isinstance(self.preferences_defaults[name], int):
+ self.prefs.SetInt(name, value)
+ elif isinstance(self.preferences_defaults[name], float):
+ self.prefs.SetFloat(name, value)
+ elif isinstance(self.preferences_defaults[name], str):
+ self.prefs.SetString(name, value)
+ else:
+ # We don't directly support any other types from the JSON file (e.g. arrays)
+ type_name = type(self.preferences_defaults[name])
+ raise RuntimeError(f"Unrecognized type for {name}: {type_name}")
+
+ def rem(self, name: str):
+ """Remove the preference. Must have an entry in the
+ addonmanager_preferences_defaults.json file."""
+ if name not in self.preferences_defaults:
+ raise RuntimeError(
+ f"Unrecognized preference {name} -- did you add "
+ + "it to addonmanager_preferences_defaults.json?"
+ )
+ if isinstance(self.preferences_defaults[name], bool):
+ return self.prefs.RemBool(name)
+ if isinstance(self.preferences_defaults[name], int):
+ return self.prefs.RemInt(name)
+ if isinstance(self.preferences_defaults[name], float):
+ return self.prefs.RemFloat(name)
+ if isinstance(self.preferences_defaults[name], str):
+ return self.prefs.RemString(name)
+ # We don't directly support any other types from the JSON file (e.g. arrays)
+ type_name = type(self.preferences_defaults[name])
+ raise RuntimeError(f"Unrecognized type for {name}: {type_name}")
+
+ @classmethod
+ def _load_preferences_defaults(cls, filename=None):
+ """Loads the preferences defaults JSON file from either a specified file, or
+ from the standard addonmanager_preferences_defaults.json file."""
+
+ if filename is None:
+ json_file = os.path.join(
+ os.path.dirname(__file__), "addonmanager_preferences_defaults.json"
+ )
+ else:
+ json_file = filename
+ with open(json_file, "r", encoding="utf-8") as f:
+ file_contents = f.read()
+ cls.preferences_defaults = json.loads(file_contents)
diff --git a/src/Mod/AddonManager/addonmanager_preferences_defaults.json b/src/Mod/AddonManager/addonmanager_preferences_defaults.json
new file mode 100644
index 0000000000..4258178ebe
--- /dev/null
+++ b/src/Mod/AddonManager/addonmanager_preferences_defaults.json
@@ -0,0 +1,43 @@
+{
+ "AddonFlagsURL": "https://raw.githubusercontent.com/FreeCAD/FreeCAD-addons/master/addonflags.json",
+ "AddonsRemoteCacheURL": "https://addons.freecad.org/metadata.zip",
+ "AddonsUpdateStatsURL": "https://addons.freecad.org/addon_update_stats.json",
+ "AutoCheck": false,
+ "BlockedMacros": "BOLTS,WorkFeatures,how to install,documentation,PartsLibrary,FCGear",
+ "CustomRepoHash": "",
+ "CustomRepositories": "",
+ "CustomToolbarName": "Auto-Created Macro Toolbar",
+ "DaysBetweenUpdates": -1,
+ "DownloadMacros": false,
+ "GitExecutable": "Not set",
+ "HideNewerFreeCADRequired": true,
+ "HideObsolete": true,
+ "HidePy2": true,
+ "KnownPythonVersions": "[]",
+ "LastCacheUpdate": "never",
+ "MacroCacheUpdateFrequency": 7,
+ "MacroGitURL": "https://github.com/FreeCAD/FreeCAD-Macros",
+ "MacroUpdateStatsURL": "https://addons.freecad.org/macro_update_stats.json",
+ "MacroWikiURL": "https://wiki.freecad.org/Macros_recipes",
+ "NoProxyCheck": "",
+ "PackageTypeSelection": 1,
+ "PrimaryAddonsSubmoduleURL": "https://raw.githubusercontent.com/FreeCAD/FreeCAD-addons/master/.gitmodules",
+ "ProxyUrl": "",
+ "PythonExecutableForPip": "Not set",
+ "RemoteIconCacheURL": "https://addons.freecad.org/icon_cache.zip",
+ "SelectedAddon": "",
+ "ShowBranchSwitcher": false,
+ "StatusSelection": 0,
+ "SystemProxyCheck": "",
+ "UpdateFrequencyComboEntry": 0,
+ "UserProxyCheck": "",
+ "ViewStyle": 1,
+ "WindowHeight": 600,
+ "WindowWidth": 800,
+ "alwaysAskForToolbar": true,
+ "devModeLastSelectedLicense": "LGPLv2.1",
+ "developerMode": false,
+ "disableGit": false,
+ "dontShowAddMacroButtonDialog": false,
+ "readWarning2022": false
+}
\ No newline at end of file