Skip to content

Commit b0d2b48

Browse files
authored
Add bindings for IFileTree glob() and walk() generators (#142)
* Adding binding for new IFileTree glob and walk. * Fix issues with Version binding.
1 parent 00a6d59 commit b0d2b48

File tree

7 files changed

+155
-10
lines changed

7 files changed

+155
-10
lines changed

src/mobase/pybind11_all.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
#include "pybind11_qt/pybind11_qt.h"
1414

1515
#include "pybind11_utils/functional.h"
16+
#include "pybind11_utils/generator.h"
1617
#include "pybind11_utils/shared_cpp_owner.h"
1718
#include "pybind11_utils/smart_variant_wrapper.h"
1819

src/mobase/wrappers/basic_classes.cpp

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,24 @@ namespace mo2::python {
5454
.value("NO_METADATA", Version::FormatMode::NoMetadata)
5555
.value("CONDENSED",
5656
static_cast<Version::FormatMode>(Version::FormatCondensed.toInt()))
57-
.export_values();
57+
.export_values()
58+
.def("__xor__",
59+
py::overload_cast<Version::FormatMode, Version::FormatModes>(
60+
&operator^))
61+
.def("__and__",
62+
py::overload_cast<Version::FormatMode, Version::FormatModes>(
63+
&operator&))
64+
.def("__or__", py::overload_cast<Version::FormatMode, Version::FormatModes>(
65+
&operator|))
66+
.def("__rxor__",
67+
py::overload_cast<Version::FormatMode, Version::FormatModes>(
68+
&operator^))
69+
.def("__rand__",
70+
py::overload_cast<Version::FormatMode, Version::FormatModes>(
71+
&operator&))
72+
.def("__ror__",
73+
py::overload_cast<Version::FormatMode, Version::FormatModes>(
74+
&operator|));
5875

5976
pyVersion
6077
.def_static("parse", &Version::parse, "value"_a,
@@ -86,8 +103,11 @@ namespace mo2::python {
86103
.def_property_readonly("subpatch", &Version::subpatch)
87104
.def_property_readonly("prereleases", &Version::preReleases)
88105
.def_property_readonly("build_metadata", &Version::buildMetadata)
89-
.def("string", &Version::string, "mode"_a = Version::FormatCondensed)
90-
.def("__str__", &Version::string)
106+
.def("string", &Version::string, "mode"_a = Version::FormatModes{})
107+
.def("__str__",
108+
[](Version const& version) {
109+
return version.string(Version::FormatCondensed);
110+
})
91111
.def(py::self < py::self)
92112
.def(py::self > py::self)
93113
.def(py::self <= py::self)

src/mobase/wrappers/pyfiletree.cpp

Lines changed: 25 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ namespace mo2::detail {
4949
return std::make_shared<PyFileTree>(parent, name, m_Callback);
5050
}
5151

52-
bool doPopulate(std::shared_ptr<const IFileTree> parent,
52+
bool doPopulate([[maybe_unused]] std::shared_ptr<const IFileTree> parent,
5353
std::vector<std::shared_ptr<FileTreeEntry>>&) const override
5454
{
5555
return true;
@@ -83,7 +83,6 @@ namespace mo2::python {
8383

8484
void add_ifiletree_bindings(pybind11::module_& m)
8585
{
86-
8786
// FileTreeEntry class:
8887
auto fileTreeEntryClass =
8988
py::class_<FileTreeEntry, std::shared_ptr<FileTreeEntry>>(m,
@@ -164,6 +163,11 @@ namespace mo2::python {
164163
.value("SKIP", IFileTree::WalkReturn::SKIP)
165164
.export_values();
166165

166+
py::enum_<IFileTree::GlobPatternType>(iFileTreeClass, "GlobPatternType")
167+
.value("GLOB", IFileTree::GlobPatternType::GLOB)
168+
.value("REGEX", IFileTree::GlobPatternType::REGEX)
169+
.export_values();
170+
167171
// Non-mutable operations:
168172
iFileTreeClass.def("exists",
169173
py::overload_cast<QString, IFileTree::FileTypes>(
@@ -175,10 +179,25 @@ namespace mo2::python {
175179
iFileTreeClass.def("pathTo", &IFileTree::pathTo, py::arg("entry"),
176180
py::arg("sep") = "\\");
177181

178-
// Note: walk() would probably be better as a generator in python, but
179-
// it is likely impossible to construct from the C++ walk() method.
180-
iFileTreeClass.def("walk", &IFileTree::walk, py::arg("callback"),
181-
py::arg("sep") = "\\");
182+
iFileTreeClass.def(
183+
"walk",
184+
py::overload_cast<
185+
std::function<IFileTree::WalkReturn(
186+
QString const&, std::shared_ptr<const FileTreeEntry>)>,
187+
QString>(&IFileTree::walk, py::const_),
188+
py::arg("callback"), py::arg("sep") = "\\");
189+
190+
iFileTreeClass.def("walk", [](IFileTree const* tree) {
191+
return make_generator(tree->walk());
192+
});
193+
194+
iFileTreeClass.def(
195+
"glob", // &IFileTree::glob,
196+
[](IFileTree const* tree, QString pattern,
197+
IFileTree::GlobPatternType patternType) {
198+
return make_generator(tree->glob(pattern, patternType));
199+
},
200+
py::arg("pattern"), py::arg("type") = IFileTree::GlobPatternType::GLOB);
182201

183202
// Kind-of-static operations:
184203
iFileTreeClass.def("createOrphanTree", &IFileTree::createOrphanTree,

src/pybind11-utils/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ cmake_minimum_required(VERSION 3.16)
22

33
add_library(pybind11-utils STATIC
44
./include/pybind11_utils/functional.h
5+
./include/pybind11_utils/generator.h
56
./include/pybind11_utils/shared_cpp_owner.h
67
./include/pybind11_utils/smart_variant_wrapper.h
78
./include/pybind11_utils/smart_variant.h
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
#ifndef PYTHON_PYBIND11_GENERATOR_H
2+
#define PYTHON_PYBIND11_GENERATOR_H
3+
4+
#include <generator>
5+
6+
#include <pybind11/pybind11.h>
7+
8+
namespace mo2::python {
9+
10+
// the code here is mostly taken from pybind11 itself, and relies on some pybind11
11+
// internals so might be subject to change when upgrading pybind11 versions
12+
13+
namespace detail {
14+
template <typename T>
15+
struct generator_state {
16+
std::generator<T> g;
17+
decltype(g.begin()) it;
18+
19+
generator_state(std::generator<T> gen) : g(std::move(gen)), it(g.begin()) {}
20+
};
21+
} // namespace detail
22+
23+
// create a Python generator from a C++ generator
24+
//
25+
template <typename T>
26+
auto make_generator(std::generator<T> g)
27+
{
28+
using state = detail::generator_state<T>;
29+
30+
namespace py = pybind11;
31+
if (!py::detail::get_type_info(typeid(state), false)) {
32+
py::class_<state>(py::handle(), "iterator", pybind11::module_local())
33+
.def("__iter__",
34+
[](state& s) -> state& {
35+
return s;
36+
})
37+
.def("__next__", [](state& s) {
38+
if (s.it != s.g.end()) {
39+
const auto v = *s.it;
40+
s.it++;
41+
return v;
42+
}
43+
else {
44+
throw py::stop_iteration();
45+
}
46+
});
47+
}
48+
49+
return py::cast(state{std::move(g)});
50+
}
51+
52+
} // namespace mo2::python
53+
54+
#endif

tests/python/CMakeLists.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ foreach (test_file ${test_files})
5353
pybind11_add_module(${target} EXCLUDE_FROM_ALL THIN_LTO ${test_file})
5454
set_target_properties(${target}
5555
PROPERTIES
56-
CXX_STANDARD 20
56+
CXX_STANDARD 23
5757
OUTPUT_NAME ${pymodule}
5858
FOLDER tests/python
5959
LIBRARY_OUTPUT_DIRECTORY "${PYLIB_DIR}/mobase_tests")

tests/python/test_filetree.py

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
from typing import TypeAlias, cast
2+
13
import mobase
24

35
import mobase_tests.filetree as m
@@ -34,3 +36,51 @@ def test_filetype():
3436

3537
assert m.is_file(FT.FILE_OR_DIRECTORY & ~FT.DIRECTORY)
3638
assert not m.is_directory(FT.FILE_OR_DIRECTORY & ~FT.DIRECTORY)
39+
40+
41+
_tree_values: TypeAlias = list["str | tuple[str, _tree_values]"]
42+
43+
44+
def make_tree(
45+
values: _tree_values, root: mobase.IFileTree | None = None
46+
) -> mobase.IFileTree:
47+
if root is None:
48+
root = cast(mobase.IFileTree, mobase.private.makeTree()) # type: ignore
49+
50+
for value in values:
51+
if isinstance(value, str):
52+
root.addFile(value)
53+
else:
54+
sub_tree = root.addDirectory(value[0])
55+
make_tree(value[1], sub_tree)
56+
57+
return root
58+
59+
60+
def test_walk():
61+
tree = make_tree(
62+
[("a", []), ("b", ["u", "v"]), "c.x", "d.y", ("e", [("q", ["c.t", ("p", [])])])]
63+
)
64+
65+
assert {"a", "b", "b/u", "b/v", "c.x", "d.y", "e", "e/q", "e/q/c.t", "e/q/p"} == {
66+
e.path("/") for e in tree.walk()
67+
}
68+
69+
entries: list[str] = []
70+
for e in tree.walk():
71+
if e.name() == "e":
72+
break
73+
entries.append(e.path("/"))
74+
assert {"a", "b", "b/u", "b/v"} == set(entries)
75+
76+
77+
def test_glob():
78+
tree = make_tree(
79+
[("a", []), ("b", ["u", "v"]), "c.x", "d.y", ("e", [("q", ["c.t", ("p", [])])])]
80+
)
81+
82+
assert {"a", "b", "b/u", "b/v", "c.x", "d.y", "e", "e/q", "e/q/c.t", "e/q/p"} == {
83+
e.path("/") for e in tree.glob("**/*")
84+
}
85+
86+
assert {"d.y"} == {e.path("/") for e in tree.glob("**/*.y")}

0 commit comments

Comments
 (0)