diff --git a/src/App/Application.cpp b/src/App/Application.cpp index 1e96d4a8fe..2a93cbcbea 100644 --- a/src/App/Application.cpp +++ b/src/App/Application.cpp @@ -101,6 +101,7 @@ #include "Annotation.h" #include "Application.h" #include "ApplicationDirectories.h" +#include "ApplicationDirectoriesPy.h" #include "CleanupProcess.h" #include "ComplexGeoData.h" #include "Services.h" @@ -331,6 +332,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.cpp b/src/App/ApplicationDirectories.cpp index 2102bd49bc..76e5d44e3d 100644 --- a/src/App/ApplicationDirectories.cpp +++ b/src/App/ApplicationDirectories.cpp @@ -568,7 +568,7 @@ void ApplicationDirectories::migrateAllPaths(const std::vector &paths) newPath = path / versionStringForPath(major, minor); } if (fs::exists(newPath)) { - throw Base::RuntimeError("Cannot migrate config - path already exists: " + Base::FileInfo::pathToString(newPath)); + continue; // Ignore an existing path: not an error, just a migration that was already done } fs::create_directories(newPath); migrateConfig(path, newPath); 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..da84f678b7 --- /dev/null +++ b/src/App/ApplicationDirectoriesPyImp.cpp @@ -0,0 +1,150 @@ +// 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 +#include +#include +#include + + +#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())) { + PyErr_SetString(PyExc_RuntimeError, "path was not a string"); + return nullptr; + } + const char* s = PyUnicode_AsUTF8(item.ptr()); + if (!s) { + return nullptr; // PyUnicode_AsUTF8 sets an error + } + paths[i] = Base::FileInfo::stringToPath(s); + } + 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 8792e7b5ae..25822fd22e 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 diff --git a/tests/src/App/ApplicationDirectories.cpp b/tests/src/App/ApplicationDirectories.cpp index 493c1348e1..9c804e7698 100644 --- a/tests/src/App/ApplicationDirectories.cpp +++ b/tests/src/App/ApplicationDirectories.cpp @@ -552,16 +552,18 @@ TEST_F(ApplicationDirectoriesTest, migrateAllPathsNonVersionedInputAppendsCurren } // Pre-existing destination -> throws Base::RuntimeError -TEST_F(ApplicationDirectoriesTest, migrateAllPathsThrowsIfDestinationAlreadyExists_NonVersioned) +TEST_F(ApplicationDirectoriesTest, migrateAllPathsIgnoresIfDestinationAlreadyExists_NonVersioned) { auto appDirs = makeAppDirsForVersion(5, 4); fs::path base = tempDir() / "exists_case"; fs::create_directories(base); - fs::create_directories(versionedPath(base, 5, 4)); // destination already exists + fs::path dest = versionedPath(base, 5, 4); + fs::create_directories(dest); // destination already exists std::vector inputs {base}; - EXPECT_THROW(appDirs->migrateAllPaths(inputs), Base::RuntimeError); + + ASSERT_NO_THROW(appDirs->migrateAllPaths(inputs)); } // Multiple inputs: one versioned, one non-versioned -> both destinations created