Add bindings for IFileTree glob() and walk() generators (#142)

* Adding binding for new IFileTree glob and walk.
* Fix issues with Version binding.
This commit is contained in:
Mikaël Capelle
2025-07-06 19:38:16 +02:00
committed by GitHub
parent 00a6d5969b
commit b0d2b48524
7 changed files with 155 additions and 10 deletions
+1
View File
@@ -13,6 +13,7 @@
#include "pybind11_qt/pybind11_qt.h"
#include "pybind11_utils/functional.h"
#include "pybind11_utils/generator.h"
#include "pybind11_utils/shared_cpp_owner.h"
#include "pybind11_utils/smart_variant_wrapper.h"
+23 -3
View File
@@ -54,7 +54,24 @@ namespace mo2::python {
.value("NO_METADATA", Version::FormatMode::NoMetadata)
.value("CONDENSED",
static_cast<Version::FormatMode>(Version::FormatCondensed.toInt()))
.export_values();
.export_values()
.def("__xor__",
py::overload_cast<Version::FormatMode, Version::FormatModes>(
&operator^))
.def("__and__",
py::overload_cast<Version::FormatMode, Version::FormatModes>(
&operator&))
.def("__or__", py::overload_cast<Version::FormatMode, Version::FormatModes>(
&operator|))
.def("__rxor__",
py::overload_cast<Version::FormatMode, Version::FormatModes>(
&operator^))
.def("__rand__",
py::overload_cast<Version::FormatMode, Version::FormatModes>(
&operator&))
.def("__ror__",
py::overload_cast<Version::FormatMode, Version::FormatModes>(
&operator|));
pyVersion
.def_static("parse", &Version::parse, "value"_a,
@@ -86,8 +103,11 @@ namespace mo2::python {
.def_property_readonly("subpatch", &Version::subpatch)
.def_property_readonly("prereleases", &Version::preReleases)
.def_property_readonly("build_metadata", &Version::buildMetadata)
.def("string", &Version::string, "mode"_a = Version::FormatCondensed)
.def("__str__", &Version::string)
.def("string", &Version::string, "mode"_a = Version::FormatModes{})
.def("__str__",
[](Version const& version) {
return version.string(Version::FormatCondensed);
})
.def(py::self < py::self)
.def(py::self > py::self)
.def(py::self <= py::self)
+25 -6
View File
@@ -49,7 +49,7 @@ namespace mo2::detail {
return std::make_shared<PyFileTree>(parent, name, m_Callback);
}
bool doPopulate(std::shared_ptr<const IFileTree> parent,
bool doPopulate([[maybe_unused]] std::shared_ptr<const IFileTree> parent,
std::vector<std::shared_ptr<FileTreeEntry>>&) const override
{
return true;
@@ -83,7 +83,6 @@ namespace mo2::python {
void add_ifiletree_bindings(pybind11::module_& m)
{
// FileTreeEntry class:
auto fileTreeEntryClass =
py::class_<FileTreeEntry, std::shared_ptr<FileTreeEntry>>(m,
@@ -164,6 +163,11 @@ namespace mo2::python {
.value("SKIP", IFileTree::WalkReturn::SKIP)
.export_values();
py::enum_<IFileTree::GlobPatternType>(iFileTreeClass, "GlobPatternType")
.value("GLOB", IFileTree::GlobPatternType::GLOB)
.value("REGEX", IFileTree::GlobPatternType::REGEX)
.export_values();
// Non-mutable operations:
iFileTreeClass.def("exists",
py::overload_cast<QString, IFileTree::FileTypes>(
@@ -175,10 +179,25 @@ namespace mo2::python {
iFileTreeClass.def("pathTo", &IFileTree::pathTo, py::arg("entry"),
py::arg("sep") = "\\");
// Note: walk() would probably be better as a generator in python, but
// it is likely impossible to construct from the C++ walk() method.
iFileTreeClass.def("walk", &IFileTree::walk, py::arg("callback"),
py::arg("sep") = "\\");
iFileTreeClass.def(
"walk",
py::overload_cast<
std::function<IFileTree::WalkReturn(
QString const&, std::shared_ptr<const FileTreeEntry>)>,
QString>(&IFileTree::walk, py::const_),
py::arg("callback"), py::arg("sep") = "\\");
iFileTreeClass.def("walk", [](IFileTree const* tree) {
return make_generator(tree->walk());
});
iFileTreeClass.def(
"glob", // &IFileTree::glob,
[](IFileTree const* tree, QString pattern,
IFileTree::GlobPatternType patternType) {
return make_generator(tree->glob(pattern, patternType));
},
py::arg("pattern"), py::arg("type") = IFileTree::GlobPatternType::GLOB);
// Kind-of-static operations:
iFileTreeClass.def("createOrphanTree", &IFileTree::createOrphanTree,
+1
View File
@@ -2,6 +2,7 @@ cmake_minimum_required(VERSION 3.16)
add_library(pybind11-utils STATIC
./include/pybind11_utils/functional.h
./include/pybind11_utils/generator.h
./include/pybind11_utils/shared_cpp_owner.h
./include/pybind11_utils/smart_variant_wrapper.h
./include/pybind11_utils/smart_variant.h
@@ -0,0 +1,54 @@
#ifndef PYTHON_PYBIND11_GENERATOR_H
#define PYTHON_PYBIND11_GENERATOR_H
#include <generator>
#include <pybind11/pybind11.h>
namespace mo2::python {
// the code here is mostly taken from pybind11 itself, and relies on some pybind11
// internals so might be subject to change when upgrading pybind11 versions
namespace detail {
template <typename T>
struct generator_state {
std::generator<T> g;
decltype(g.begin()) it;
generator_state(std::generator<T> gen) : g(std::move(gen)), it(g.begin()) {}
};
} // namespace detail
// create a Python generator from a C++ generator
//
template <typename T>
auto make_generator(std::generator<T> g)
{
using state = detail::generator_state<T>;
namespace py = pybind11;
if (!py::detail::get_type_info(typeid(state), false)) {
py::class_<state>(py::handle(), "iterator", pybind11::module_local())
.def("__iter__",
[](state& s) -> state& {
return s;
})
.def("__next__", [](state& s) {
if (s.it != s.g.end()) {
const auto v = *s.it;
s.it++;
return v;
}
else {
throw py::stop_iteration();
}
});
}
return py::cast(state{std::move(g)});
}
} // namespace mo2::python
#endif
+1 -1
View File
@@ -53,7 +53,7 @@ foreach (test_file ${test_files})
pybind11_add_module(${target} EXCLUDE_FROM_ALL THIN_LTO ${test_file})
set_target_properties(${target}
PROPERTIES
CXX_STANDARD 20
CXX_STANDARD 23
OUTPUT_NAME ${pymodule}
FOLDER tests/python
LIBRARY_OUTPUT_DIRECTORY "${PYLIB_DIR}/mobase_tests")
+50
View File
@@ -1,3 +1,5 @@
from typing import TypeAlias, cast
import mobase
import mobase_tests.filetree as m
@@ -34,3 +36,51 @@ def test_filetype():
assert m.is_file(FT.FILE_OR_DIRECTORY & ~FT.DIRECTORY)
assert not m.is_directory(FT.FILE_OR_DIRECTORY & ~FT.DIRECTORY)
_tree_values: TypeAlias = list["str | tuple[str, _tree_values]"]
def make_tree(
values: _tree_values, root: mobase.IFileTree | None = None
) -> mobase.IFileTree:
if root is None:
root = cast(mobase.IFileTree, mobase.private.makeTree()) # type: ignore
for value in values:
if isinstance(value, str):
root.addFile(value)
else:
sub_tree = root.addDirectory(value[0])
make_tree(value[1], sub_tree)
return root
def test_walk():
tree = make_tree(
[("a", []), ("b", ["u", "v"]), "c.x", "d.y", ("e", [("q", ["c.t", ("p", [])])])]
)
assert {"a", "b", "b/u", "b/v", "c.x", "d.y", "e", "e/q", "e/q/c.t", "e/q/p"} == {
e.path("/") for e in tree.walk()
}
entries: list[str] = []
for e in tree.walk():
if e.name() == "e":
break
entries.append(e.path("/"))
assert {"a", "b", "b/u", "b/v"} == set(entries)
def test_glob():
tree = make_tree(
[("a", []), ("b", ["u", "v"]), "c.x", "d.y", ("e", [("q", ["c.t", ("p", [])])])]
)
assert {"a", "b", "b/u", "b/v", "c.x", "d.y", "e", "e/q", "e/q/c.t", "e/q/p"} == {
e.path("/") for e in tree.glob("**/*")
}
assert {"d.y"} == {e.path("/") for e in tree.glob("**/*.y")}