Merge pull request #23885 from chennes/pythonInterfaceToApplicationDirectories

App: Add Python interface to ApplicationDirectories
This commit is contained in:
sliptonic
2025-09-17 18:11:41 -05:00
committed by GitHub
6 changed files with 269 additions and 4 deletions

View File

@@ -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");

View File

@@ -568,7 +568,7 @@ void ApplicationDirectories::migrateAllPaths(const std::vector<fs::path> &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);

View File

@@ -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).
"""
...

View File

@@ -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 <https://www.gnu.org/licenses/>. *
* *
**************************************************************************************************/
#include <Python.h>
#include <filesystem>
#include <string>
#include <vector>
#include "Application.h"
// inclusion of the generated files (generated out of ApplicationDirectories.pyi)
#include <App/ApplicationDirectoriesPy.h>
#include <App/ApplicationDirectoriesPy.cpp> // 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 {"<ApplicationDirectoriesPy object>"};
}
[[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<fs::path> 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)

View File

@@ -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

View File

@@ -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<fs::path> 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