Skip to content

Commit

Permalink
nb_bound_method: add introspection attributes and fix surprising beha…
Browse files Browse the repository at this point in the history
…vior around wrapper descriptors (#216)

* nb_bound_method: add introspection attributes and fix surprising behavior around wrapper descriptors
  • Loading branch information
oremanj authored May 24, 2023
1 parent 936bfa5 commit 0a6f8f3
Show file tree
Hide file tree
Showing 4 changed files with 42 additions and 2 deletions.
6 changes: 6 additions & 0 deletions docs/changelog.rst
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,12 @@ Version 1.3.0 (TBD)
on object ownership <enable_shared_from_this>` for more details.
(PR `#212 <https://github.com/wjakob/nanobind/pull/212>`__).

* Added introspection attributes ``__self__`` and ``__func__`` to nanobind
bound methods, to make them more like regular Python bound methods.
Fixed a bug where ``some_obj.method.__call__()`` would behave differently
than ``some_obj.method()``.
(PR `#216 <https://github.com/wjakob/nanobind/pull/216>`__).

* ABI version 8.

Version 1.2.0 (April 24, 2023)
Expand Down
17 changes: 15 additions & 2 deletions src/nb_func.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1128,9 +1128,22 @@ PyObject *nb_func_getattro(PyObject *self, PyObject *name_) {
return PyObject_GenericGetAttr(self, name_);
}

PyObject *nb_bound_method_getattro(PyObject *self, PyObject *name) {
PyObject *nb_bound_method_getattro(PyObject *self, PyObject *name_) {
bool passthrough = false;
if (const char *name = PyUnicode_AsUTF8AndSize(name_, nullptr)) {
// These attributes do exist on nb_bound_method (because they
// exist on every type) but we want to take their special handling
// from nb_func_getattro instead.
passthrough = (strcmp(name, "__doc__") == 0 ||
strcmp(name, "__module__") == 0);
}
if (!passthrough) {
if (PyObject* res = PyObject_GenericGetAttr(self, name_))
return res;
PyErr_Clear();
}
nb_func *func = ((nb_bound_method *) self)->func;
return nb_func_getattro((PyObject *) func, name);
return nb_func_getattro((PyObject *) func, name_);
}

/// Excise a substring from 's'
Expand Down
4 changes: 4 additions & 0 deletions src/nb_internals.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,10 @@ static PyType_Spec nb_method_spec = {
static PyMemberDef nb_bound_method_members[] = {
{ "__vectorcalloffset__", T_PYSSIZET,
(Py_ssize_t) offsetof(nb_bound_method, vectorcall), READONLY, nullptr },
{ "__func__", T_OBJECT_EX,
(Py_ssize_t) offsetof(nb_bound_method, func), READONLY, nullptr },
{ "__self__", T_OBJECT_EX,
(Py_ssize_t) offsetof(nb_bound_method, self), READONLY, nullptr },
{ nullptr, 0, 0, 0, nullptr }
};

Expand Down
17 changes: 17 additions & 0 deletions tests/test_classes.py
Original file line number Diff line number Diff line change
Expand Up @@ -607,6 +607,7 @@ def test33_polymorphic_downcast():
assert isinstance(t.polymorphic_factory(), t.PolymorphicSubclass)
assert isinstance(t.polymorphic_factory_2(), t.PolymorphicBase)


def test34_trampoline_optimization():
class Rufus(t.Dog):
def __init__(self):
Expand All @@ -631,3 +632,19 @@ def name(self):
assert t.go(d2) == 'Rufus says woof'
finally:
t.Dog.name = old


def test35_method_introspection():
obj = t.Struct(5)
m = obj.value
assert m() == m.__call__() == 5
assert hash(m) == m.__hash__()
assert repr(m) == m.__repr__()
assert "bound_method" in repr(m)
assert m.__self__ is obj
assert m.__func__ is t.Struct.value
# attributes not defined by nb_bound_method are forwarded to nb_method:
assert m.__name__ == "value"
assert m.__qualname__ == "Struct.value"
assert m.__module__ == t.__name__
assert m.__doc__ == t.Struct.value.__doc__ == "value(self) -> int"

0 comments on commit 0a6f8f3

Please sign in to comment.