diff --git a/docs/changelog.rst b/docs/changelog.rst index 478b7d7ab..7f898fe55 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -243,6 +243,11 @@ v2.2.0 (Not yet released) * Fixed implicit conversion of `py::enum_` to integer types on Python 2.7. `#821 `_. +* Added ``py::hash`` to fetch the hash value of Python objects, and + ``.def(hash(py::self))`` to provide the C++ ``std::hash`` as the Python + ``__hash__`` method. + `#1034 `_. + * Fixed ``__truediv__`` on Python 2 and ``__itruediv__`` on Python 3. `#867 `_. diff --git a/include/pybind11/operators.h b/include/pybind11/operators.h index 11ea24850..b3dd62c3b 100644 --- a/include/pybind11/operators.h +++ b/include/pybind11/operators.h @@ -28,7 +28,7 @@ enum op_id : int { op_int, op_long, op_float, op_str, op_cmp, op_gt, op_ge, op_lt, op_le, op_eq, op_ne, op_iadd, op_isub, op_imul, op_idiv, op_imod, op_ilshift, op_irshift, op_iand, op_ixor, op_ior, op_complex, op_bool, op_nonzero, - op_repr, op_truediv, op_itruediv + op_repr, op_truediv, op_itruediv, op_hash }; enum op_type : int { @@ -148,6 +148,7 @@ PYBIND11_INPLACE_OPERATOR(ior, operator|=, l |= r) PYBIND11_UNARY_OPERATOR(neg, operator-, -l) PYBIND11_UNARY_OPERATOR(pos, operator+, +l) PYBIND11_UNARY_OPERATOR(abs, abs, std::abs(l)) +PYBIND11_UNARY_OPERATOR(hash, hash, std::hash()(l)) PYBIND11_UNARY_OPERATOR(invert, operator~, (~l)) PYBIND11_UNARY_OPERATOR(bool, operator!, !!l) PYBIND11_UNARY_OPERATOR(int, int_, (int) l) diff --git a/include/pybind11/pytypes.h b/include/pybind11/pytypes.h index 9e18a019b..92316c38c 100644 --- a/include/pybind11/pytypes.h +++ b/include/pybind11/pytypes.h @@ -390,6 +390,13 @@ inline void setattr(handle obj, handle name, handle value) { inline void setattr(handle obj, const char *name, handle value) { if (PyObject_SetAttrString(obj.ptr(), name, value.ptr()) != 0) { throw error_already_set(); } } + +inline ssize_t hash(handle obj) { + auto h = PyObject_Hash(obj.ptr()); + if (h == -1) { throw error_already_set(); } + return h; +} + /// @} python_builtins NAMESPACE_BEGIN(detail) diff --git a/tests/test_operator_overloading.cpp b/tests/test_operator_overloading.cpp index d4d35f0cf..4ad34d104 100644 --- a/tests/test_operator_overloading.cpp +++ b/tests/test_operator_overloading.cpp @@ -10,6 +10,7 @@ #include "pybind11_tests.h" #include "constructor_stats.h" #include +#include class Vector2 { public: @@ -53,6 +54,14 @@ int operator+(const C2 &, const C2 &) { return 22; } int operator+(const C2 &, const C1 &) { return 21; } int operator+(const C1 &, const C2 &) { return 12; } +namespace std { + template<> + struct hash { + // Not a good hash function, but easy to test + size_t operator()(const Vector2 &) { return 4; } + }; +} + TEST_SUBMODULE(operators, m) { // test_operator_overloading @@ -77,6 +86,7 @@ TEST_SUBMODULE(operators, m) { .def(float() * py::self) .def(float() / py::self) .def("__str__", &Vector2::toString) + .def(hash(py::self)) ; m.attr("Vector") = m.attr("Vector2"); diff --git a/tests/test_operator_overloading.py b/tests/test_operator_overloading.py index 845ceddf3..0d80e5ed3 100644 --- a/tests/test_operator_overloading.py +++ b/tests/test_operator_overloading.py @@ -35,6 +35,8 @@ def test_operator_overloading(): v2 /= v1 assert str(v2) == "[2.000000, 8.000000]" + assert hash(v1) == 4 + cstats = ConstructorStats.get(m.Vector2) assert cstats.alive() == 2 del v1 diff --git a/tests/test_pytypes.cpp b/tests/test_pytypes.cpp index edc1e995c..474223674 100644 --- a/tests/test_pytypes.cpp +++ b/tests/test_pytypes.cpp @@ -261,4 +261,6 @@ TEST_SUBMODULE(pytypes, m) { }); m.def("print_failure", []() { py::print(42, UnregisteredType()); }); + + m.def("hash_function", [](py::object obj) { return py::hash(obj); }); } diff --git a/tests/test_pytypes.py b/tests/test_pytypes.py index b8dad8412..2487cf666 100644 --- a/tests/test_pytypes.py +++ b/tests/test_pytypes.py @@ -220,3 +220,19 @@ def test_print(capture): if debug_enabled else "arguments to Python object (compile in debug mode for details)" ) + + +def test_hash(): + class Hashable(object): + def __init__(self, value): + self.value = value + + def __hash__(self): + return self.value + + class Unhashable(object): + __hash__ = None + + assert m.hash_function(Hashable(42)) == 42 + with pytest.raises(TypeError): + m.hash_function(Unhashable())