Merge pull request #23885 from chennes/pythonInterfaceToApplicationDirectories
App: Add Python interface to ApplicationDirectories
This commit is contained in:
@@ -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");
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
108
src/App/ApplicationDirectories.pyi
Normal file
108
src/App/ApplicationDirectories.pyi
Normal 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).
|
||||
"""
|
||||
...
|
||||
150
src/App/ApplicationDirectoriesPyImp.cpp
Normal file
150
src/App/ApplicationDirectoriesPyImp.cpp
Normal 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)
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user