Skip to content

Support frozenset, tuple as dict keys (alternative approach) #3887

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 21 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
a4b5628
Support frozenset, tuple as dict keys
ecatmur Apr 19, 2022
1a0fa08
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Apr 20, 2022
ce1dff9
C++11
ecatmur Apr 20, 2022
26a2df5
C++11, again
ecatmur Apr 20, 2022
a7cabff
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Apr 20, 2022
c82d7a1
clang-tidy
ecatmur Apr 20, 2022
ec50f3b
Merge branch 'frozenset-v2' of https://github.com/ecatmur/pybind11 in…
ecatmur Apr 20, 2022
43ca8f5
More tests for tuple -> list, frozenset -> set
ecatmur Apr 21, 2022
e828031
Add frozenset, and allow it cast to std::set
ecatmur Apr 24, 2022
f2db7bb
Rename set_base to any_set to match Python C API
ecatmur Apr 24, 2022
ef92aa5
PR: static_cast, anyset
ecatmur May 1, 2022
736f293
Add tests for frozenset
ecatmur May 1, 2022
faf8a51
Remove frozenset default ctor, add tests
ecatmur May 1, 2022
05b6147
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] May 1, 2022
26a29f4
Merge remote-tracking branch 'upstream/master' into frozenset-core
ecatmur May 1, 2022
713dd5b
Merge branch 'frozenset-core' into frozenset-v2
ecatmur May 1, 2022
0cf2cae
Add rationale to `pyobject_caster` default ctor
ecatmur May 2, 2022
c63ca46
Remove ineffectual protected: access control
ecatmur May 4, 2022
7ac7461
Merge remote-tracking branch 'origin/frozenset-core' into frozenset-v2
ecatmur May 15, 2022
0c1e035
Merge remote-tracking branch 'upstream/master' into frozenset-v2
ecatmur May 15, 2022
fab3abb
Merge remote-tracking branch 'upstream/master' into frozenset-v2
ecatmur May 27, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions include/pybind11/cast.h
Original file line number Diff line number Diff line change
Expand Up @@ -1639,6 +1639,27 @@ object object_api<Derived>::call(Args &&...args) const {
return operator()<policy>(std::forward<Args>(args)...);
}

// Convert list -> tuple and set -> frozenset for use as keys in dict, set etc.
// https://mail.python.org/pipermail/python-dev/2005-October/057586.html
inline object freeze(object &&obj) {
if (isinstance<list>(obj)) {
return tuple(std::move(obj));
}
if (isinstance<set>(obj)) {
return frozenset(std::move(obj));
}
return std::move(obj);
}

template <typename Caster, typename SFINAE = void>
struct frozen_type_name {
static constexpr auto name = Caster::name;
};
template <typename Caster>
struct frozen_type_name<Caster, void_t<decltype(Caster::frozen_name)>> {
static constexpr auto name = Caster::frozen_name;
};

PYBIND11_NAMESPACE_END(detail)

template <typename T>
Expand Down
20 changes: 15 additions & 5 deletions include/pybind11/stl.h
Original file line number Diff line number Diff line change
Expand Up @@ -79,14 +79,17 @@ struct set_caster {
for (auto &&value : src) {
auto value_ = reinterpret_steal<object>(
key_conv::cast(forward_like<T>(value), policy, parent));
if (!value_ || !s.add(std::move(value_))) {
if (!value_ || !s.add(freeze(std::move(value_)))) {
return handle();
}
}
return s.release();
}

PYBIND11_TYPE_CASTER(type, const_name("Set[") + key_conv::name + const_name("]"));
PYBIND11_TYPE_CASTER(type,
const_name("Set[") + frozen_type_name<key_conv>::name + const_name("]"));
static constexpr auto frozen_name
= const_name("FrozenSet[") + frozen_type_name<key_conv>::name + const_name("]");
};

template <typename Type, typename Key, typename Value>
Expand Down Expand Up @@ -128,14 +131,14 @@ struct map_caster {
if (!key || !value) {
return handle();
}
d[key] = value;
d[freeze(std::move(key))] = std::move(value);
}
return d.release();
}

PYBIND11_TYPE_CASTER(Type,
const_name("Dict[") + key_conv::name + const_name(", ") + value_conv::name
+ const_name("]"));
const_name("Dict[") + frozen_type_name<key_conv>::name + const_name(", ")
+ value_conv::name + const_name("]"));
};

template <typename Type, typename Value>
Expand Down Expand Up @@ -188,6 +191,8 @@ struct list_caster {
}

PYBIND11_TYPE_CASTER(Type, const_name("List[") + value_conv::name + const_name("]"));
static constexpr auto frozen_name
= const_name("Tuple[") + value_conv::name + const_name(", ...]");
};

template <typename Type, typename Alloc>
Expand Down Expand Up @@ -257,6 +262,11 @@ struct array_caster {
const_name("[") + const_name<Size>()
+ const_name("]"))
+ const_name("]"));
static constexpr auto frozen_name
= const_name("Tuple[") + value_conv::name
+ const_name<Resizable>(const_name(", ..."),
const_name("[") + const_name<Size>() + const_name("]"))
+ const_name("]");
};

template <typename Type, size_t Size>
Expand Down
16 changes: 16 additions & 0 deletions tests/test_stl.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -249,6 +249,22 @@ TEST_SUBMODULE(stl, m) {
return v;
});

// test_frozen_key
m.def("cast_set_map", []() {
return std::map<std::set<std::string>, std::string>{{{"key1", "key2"}, "value"}};
});
m.def("load_set_map", [](const std::map<std::set<std::string>, std::string> &map) {
return map.at({"key1", "key2"}) == "value" && map.at({"key3"}) == "value2";
});
m.def("cast_set_set", []() { return std::set<std::set<std::string>>{{"key1", "key2"}}; });
m.def("load_set_set", [](const std::set<std::set<std::string>> &set) {
return (set.count({"key1", "key2"}) != 0u) && (set.count({"key3"}) != 0u);
});
m.def("cast_vector_set", []() { return std::set<std::vector<int>>{{1, 2}}; });
m.def("load_vector_set", [](const std::set<std::vector<int>> &set) {
return (set.count({1, 2}) != 0u) && (set.count({3}) != 0u);
});

pybind11::enum_<EnumType>(m, "EnumType")
.value("kSet", EnumType::kSet)
.value("kUnset", EnumType::kUnset);
Expand Down
32 changes: 32 additions & 0 deletions tests/test_stl.py
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,38 @@ def test_recursive_casting():
assert z[0].value == 7 and z[1].value == 42


def test_frozen_key(doc):
"""Test that we special-case C++ key types to Python immutable containers, e.g.:
std::map<std::set<K>, V> <-> dict[frozenset[K], V]
std::set<std::set<T>> <-> set[frozenset[T]]
std::set<std::vector<T>> <-> set[tuple[T, ...]]
"""
s = m.cast_set_map()
assert s == {frozenset({"key1", "key2"}): "value"}
s[frozenset({"key3"})] = "value2"
assert m.load_set_map(s)
assert doc(m.cast_set_map) == "cast_set_map() -> Dict[FrozenSet[str], str]"
assert (
doc(m.load_set_map) == "load_set_map(arg0: Dict[FrozenSet[str], str]) -> bool"
)

s = m.cast_set_set()
assert s == {frozenset({"key1", "key2"})}
s.add(frozenset({"key3"}))
assert m.load_set_set(s)
assert doc(m.cast_set_set) == "cast_set_set() -> Set[FrozenSet[str]]"
assert doc(m.load_set_set) == "load_set_set(arg0: Set[FrozenSet[str]]) -> bool"

s = m.cast_vector_set()
assert s == {(1, 2)}
s.add((3,))
assert m.load_vector_set(s)
assert doc(m.cast_vector_set) == "cast_vector_set() -> Set[Tuple[int, ...]]"
assert (
doc(m.load_vector_set) == "load_vector_set(arg0: Set[Tuple[int, ...]]) -> bool"
)


def test_move_out_container():
"""Properties use the `reference_internal` policy by default. If the underlying function
returns an rvalue, the policy is automatically changed to `move` to avoid referencing
Expand Down