diff --git a/src/App/Application.cpp b/src/App/Application.cpp
index a5dd147c00..290f2099a9 100644
--- a/src/App/Application.cpp
+++ b/src/App/Application.cpp
@@ -103,6 +103,7 @@
#include "Annotation.h"
#include "Application.h"
#include "ApplicationDirectories.h"
+#include "ApplicationDirectoriesPy.h"
#include "CleanupProcess.h"
#include "ComplexGeoData.h"
#include "Services.h"
@@ -333,6 +334,7 @@ void Application::setupPythonTypes()
Base::InterpreterSingleton::addType(&Base::TypePy ::Type,pBaseModule,"TypeId");
Base::InterpreterSingleton::addType(&Base::PrecisionPy ::Type,pBaseModule,"Precision");
+ Base::InterpreterSingleton::addType(&ApplicationDirectoriesPy::Type, pAppModule, "ApplicationDirectories");
Base::InterpreterSingleton::addType(&MaterialPy::Type, pAppModule, "Material");
Base::InterpreterSingleton::addType(&MetadataPy::Type, pAppModule, "Metadata");
diff --git a/src/App/ApplicationDirectories.pyi b/src/App/ApplicationDirectories.pyi
new file mode 100644
index 0000000000..ee3419eda8
--- /dev/null
+++ b/src/App/ApplicationDirectories.pyi
@@ -0,0 +1,108 @@
+from Base.PyObjectBase import PyObjectBase
+from typing import List
+
+
+class ApplicationDirectories(PyObjectBase):
+ """
+ App.ApplicationDirectories class.
+
+ For the time being this class only provides access to the directory versioning methods of its
+ C++ counterpart. These are all static methods, so no instance is needed. The main methods of
+ this class are migrateAllPaths(), usingCurrentVersionConfig(), and versionStringForPath().
+
+ Author: Chris Hennes (chennes@pioneerlibrarysystem.org)
+ Licence: LGPL-2.1-or-later
+ DeveloperDocu: ApplicationDirectories
+ """
+
+ @staticmethod
+ def usingCurrentVersionConfig(path:str) -> bool:
+ """
+ usingCurrentVersionConfig(path)
+
+ Determine if a given config path is for the current version of the program
+
+ path : the path to check
+ """
+ ...
+
+ @staticmethod
+ def migrateAllPaths(paths: List[str]) -> None:
+ """
+ migrateAllPaths(paths)
+
+ Migrate a set of versionable configuration directories from the given paths to a new
+ version. The new version's directories cannot exist yet, and the old ones *must* exist.
+ If the old paths are themselves versioned, then the new paths will be placed at the same
+ level in the directory structure (e.g., they will be siblings of each entry in paths).
+ If paths are NOT versioned, the new (versioned) copies will be placed *inside* the
+ original paths.
+
+ If the list contains the same path multiple times, the duplicates are ignored, so it is safe
+ to pass the same path multiple times.
+
+ Examples:
+ Running FreeCAD 1.1, /usr/share/FreeCAD/Config/ -> /usr/share/FreeCAD/Config/v1-1/
+ Running FreeCAD 1.1, /usr/share/FreeCAD/Config/v1-1 -> raises exception, path exists
+ Running FreeCAD 1.2, /usr/share/FreeCAD/Config/v1-1/ -> /usr/share/FreeCAD/Config/v1-2/
+ """
+ ...
+
+ @staticmethod
+ def versionStringForPath(major:int, minor:int) -> str:
+ """
+ versionStringForPath(major, minor) -> str
+
+ Given a major and minor version number, return a string that can be used as the name for a
+ versioned subdirectory. Only returns the version string, not the full path.
+ """
+ ...
+
+ @staticmethod
+ def isVersionedPath(startingPath:str) -> bool:
+ """
+ isVersionedPath(startingPath) -> bool
+
+ Determine if a given path is versioned (that is, if its last component contains
+ something that this class would have created as a versioned subdirectory). Returns true
+ for any path that the *current* version of FreeCAD would recognized as versioned, and false
+ for either something that is not versioned, or something that is versioned but for a later
+ version of FreeCAD.
+ """
+ ...
+
+ @staticmethod
+ def mostRecentAvailableConfigVersion(startingPath:str) -> str:
+ """
+ mostRecentAvailableConfigVersion(startingPath) -> str
+
+ Given a base path that is expected to contain versioned subdirectories, locate the
+ directory name (*not* the path, only the final component, the version string itself)
+ corresponding to the most recent version of the software, up to and including the current
+ running version, but NOT exceeding it -- any *later* version whose directories exist
+ in the path is ignored. See also mostRecentConfigFromBase().
+ """
+ ...
+
+ @staticmethod
+ def mostRecentConfigFromBase(startingPath: str) -> str:
+ """
+ mostRecentConfigFromBase(startingPath) -> str
+
+ Given a base path that is expected to contained versioned subdirectories, locate the
+ directory corresponding to the most recent version of the software, up to and including
+ the current version, but NOT exceeding it. Returns the complete path, not just the final
+ component. See also mostRecentAvailableConfigVersion().
+ """
+ ...
+
+ @staticmethod
+ def migrateConfig(oldPath: str, newPath: str) -> None:
+ """
+ migrateConfig(oldPath, newPath) -> None
+
+ A utility method to copy all files and directories from oldPath to newPath, handling the
+ case where newPath might itself be a subdirectory of oldPath (and *not* attempting that
+ otherwise-recursive copy).
+ """
+ ...
diff --git a/src/App/ApplicationDirectoriesPyImp.cpp b/src/App/ApplicationDirectoriesPyImp.cpp
new file mode 100644
index 0000000000..2b1b0e8a48
--- /dev/null
+++ b/src/App/ApplicationDirectoriesPyImp.cpp
@@ -0,0 +1,154 @@
+// SPDX-License-Identifier: LGPL-2.1-or-later
+/***************************************************************************************************
+ * *
+ * Copyright (c) 2025 The FreeCAD project association AISBL *
+ * *
+ * 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 . *
+ * *
+ **************************************************************************************************/
+
+#include "PreCompiled.h" // NOLINT
+#ifndef _PreComp_
+#include
+#include
+#include
+#include
+#endif
+
+
+#include "Application.h"
+
+// inclusion of the generated files (generated out of ApplicationDirectories.pyi)
+#include
+#include // NOLINT
+
+namespace fs = std::filesystem;
+
+using namespace App;
+
+// NOLINTBEGIN(cppcoreguidelines-pro-type-vararg)
+
+// returns a string which represent the object e.g. when printed in python
+std::string ApplicationDirectoriesPy::representation() const
+{
+ return {""};
+}
+
+[[maybe_unused]] PyObject* ApplicationDirectoriesPy::usingCurrentVersionConfig(PyObject* args)
+{
+ char *path = nullptr;
+ if (!PyArg_ParseTuple(args, "s", &path)) {
+ return nullptr;
+ }
+ bool result = App::Application::directories()->usingCurrentVersionConfig(Base::FileInfo::stringToPath(path));
+ return Py::new_reference_to(Py::Boolean(result));
+}
+
+[[maybe_unused]] PyObject* ApplicationDirectoriesPy::migrateAllPaths(PyObject* args)
+{
+ PyObject* object {nullptr};
+ if (!PyArg_ParseTuple(args, "O", &object)) {
+ return nullptr;
+ }
+
+ if (PyTuple_Check(object) || PyList_Check(object)) {
+ Py::Sequence seq(object);
+ Py::Sequence::size_type size = seq.size();
+ std::vector paths;
+ paths.resize(size);
+
+ for (Py::Sequence::size_type i = 0; i < size; i++) {
+ Py::Object item = seq[i];
+ if (PyUnicode_Check(item.ptr())) {
+ const char* s = PyUnicode_AsUTF8(item.ptr());
+ if (!s) {
+ return nullptr; // PyUnicode_AsUTF8 sets an error
+ }
+ paths[i] = Base::FileInfo::stringToPath(s);
+ }
+ else {
+ PyErr_SetString(PyExc_RuntimeError, "path was not a string");
+ return nullptr;
+ }
+ }
+ App::Application::directories()->migrateAllPaths(paths);
+ }
+ Py_Return;
+}
+
+[[maybe_unused]] PyObject* ApplicationDirectoriesPy::versionStringForPath(PyObject* args)
+{
+ int major {0};
+ int minor {0};
+ if (!PyArg_ParseTuple(args, "ii", &major, &minor)) {
+ return nullptr;
+ }
+ auto result = App::ApplicationDirectories::versionStringForPath(major, minor);
+ return Py::new_reference_to(Py::String(result));
+}
+
+[[maybe_unused]] PyObject* ApplicationDirectoriesPy::isVersionedPath(PyObject* args)
+{
+ char *path = nullptr;
+ if (!PyArg_ParseTuple(args, "s", &path)) {
+ return nullptr;
+ }
+ App::Application::directories()->isVersionedPath(Base::FileInfo::stringToPath(path));
+ Py_Return;
+}
+
+[[maybe_unused]] PyObject* ApplicationDirectoriesPy::mostRecentAvailableConfigVersion(PyObject* args)
+{
+ char *path = nullptr;
+ if (!PyArg_ParseTuple(args, "s", &path)) {
+ return nullptr;
+ }
+ std::string result = App::Application::directories()->mostRecentAvailableConfigVersion(Base::FileInfo::stringToPath(path));
+ return Py::new_reference_to(Py::String(result));
+}
+
+[[maybe_unused]] PyObject* ApplicationDirectoriesPy::mostRecentConfigFromBase(PyObject* args)
+{
+ char *path = nullptr;
+ if (!PyArg_ParseTuple(args, "s", &path)) {
+ return nullptr;
+ }
+ fs::path result = App::Application::directories()->mostRecentConfigFromBase(Base::FileInfo::stringToPath(path));
+ return Py::new_reference_to(Py::String(Base::FileInfo::pathToString(result)));
+}
+
+[[maybe_unused]] PyObject* ApplicationDirectoriesPy::migrateConfig(PyObject* args)
+{
+ char *oldPath = nullptr;
+ char *newPath = nullptr;
+ if (!PyArg_ParseTuple(args, "ss", &oldPath, &newPath)) {
+ return nullptr;
+ }
+ App::ApplicationDirectories::migrateConfig(
+ Base::FileInfo::stringToPath(oldPath),
+ Base::FileInfo::stringToPath(newPath));
+ Py_Return;
+}
+
+PyObject* ApplicationDirectoriesPy::getCustomAttributes([[maybe_unused]] const char* attr) const
+{
+ return nullptr;
+}
+
+int ApplicationDirectoriesPy::setCustomAttributes([[maybe_unused]] const char* attr, [[maybe_unused]] PyObject* obj)
+{
+ return 0;
+}
+
+// NOLINTEND(cppcoreguidelines-pro-type-vararg)
diff --git a/src/App/CMakeLists.txt b/src/App/CMakeLists.txt
index 97e372b07e..9035ddd607 100644
--- a/src/App/CMakeLists.txt
+++ b/src/App/CMakeLists.txt
@@ -86,6 +86,7 @@ list(APPEND FreeCADApp_LIBS
${QtXml_LIBRARIES}
)
+generate_from_py(ApplicationDirectories)
generate_from_py(Document)
generate_from_py(DocumentObject)
generate_from_py(Extension)
@@ -112,6 +113,7 @@ generate_embed_from_py(FreeCADInit InitScript.h)
generate_embed_from_py(FreeCADTest TestScript.h)
SET(FreeCADApp_Pyi_SRCS
+ ApplicationDirectories.pyi
Extension.pyi
ExtensionContainer.pyi
DocumentObjectExtension.pyi
@@ -281,6 +283,7 @@ SET(FreeCADApp_CPP_SRCS
${Properties_CPP_SRCS}
Application.cpp
ApplicationDirectories.cpp
+ ApplicationDirectoriesPyImp.cpp
ApplicationPy.cpp
AutoTransaction.cpp
Branding.cpp