-
Notifications
You must be signed in to change notification settings - Fork 2.1k
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
Better error message when inheriting from a type without a constructor #2432
base: master
Are you sure you want to change the base?
Conversation
773edfb
to
1a8e2d0
Compare
This is a bit hacky -- the thing that this is trying to avoid is taking the address of the inline function
|
8374d97
to
d5bffa9
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Huh, curious as to how and why this works? I thought tp_init
was always copied from the superclass and just overwritten by "dynamically defined" __init__
methods?
I'll have a look in closer detail, later, to understand everything, but I'd be up for including this! :-)
Trying this: PYBIND11_MODULE(example, m)
{
py::class_<X/*, std::shared_ptr<X>*/> classX(m, "X");
printf("%p %p\n", ((PyHeapTypeObject*) py::detail::get_internals().instance_base)->ht_type.tp_init, ((PyHeapTypeObject*) classX.$
classX.def(py::init<>());
printf("%p %p\n", ((PyHeapTypeObject*) py::detail::get_internals().instance_base)->ht_type.tp_init, ((PyHeapTypeObject*) classX.$
} shows that CPython indeed changes the $ python3.8
Python 3.8.0 (default, Oct 28 2019, 16:14:01)
[GCC 8.3.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> import example
0x7fd980853919 0x7fd980853919
0x7fd980853919 0x5b0400 but PyPy doesn't: $ pypy3
Python 3.6.9 (1608da62bfc7, Dec 23 2019, 10:50:04)
[PyPy 7.3.0 with GCC 7.3.1 20180303 (Red Hat 7.3.1-5)] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>>> import example
0x7f247d676608 0x7f247d676608
0x7f247d676608 0x7f247d676608
>>>> example.X.__init__
<<instancemethod __init__> at 0x00007f247d902e00> So is this in the Python specification, or is this just an implementation detail of CPython? Also, not sure how I feel about that myself, but just throwing this out there: similarly to |
Hmmm ... what if we just tweaked the error message slightly, to express explicitly that
I think that'd be >90% of the gain for very little effort and no risk. The only doubt I had in connection with open_spiel: what if the extension class has no |
At the risk of being overly blunt/critical/pedantic: isn't the fact that a method must exist a rather implicit when a messages says it must be called? The main advantage I saw for this exception (for users of a pybind11-library, not for developers; those should just read the docs to know how to make some type subclassable), was that it would make clear they cannot subclass that type. But if that's not consistently possible, I'd try to keep the message as simple as possible. |
This extra enhancement is just an additional debugging tool when users do something they shouldn't. As such, I'm fine with it only working on CPython, even if it happens to be an implementation detail. If the detail changes, the TypeError will still occur, and that's the most important part to me as it keeps debugging at the Python level instead of the C++ level for well-formed bindings. |
d5bffa9
to
49aa611
Compare
You're missing the point. |
@virtuald Rather than checking on construction, I was playing around with adding this check when subclassing. To my surprise, this works: /// metaclass `__new__` function that is used to create all pybind11 classes.
extern "C" inline PyObject *pybind11_meta_new(PyTypeObject *metacls, PyObject *args, PyObject *kwargs) {
// use the default metaclass new to create the type
PyObject *type = PyType_Type.tp_new(metacls, args, kwargs);
if (type == nullptr) {
return nullptr;
}
#if !defined(PYPY_VERSION)
auto default_init = ((PyHeapTypeObject*)get_internals().instance_base)->ht_type.tp_init;
for (handle base : reinterpret_borrow<tuple>(((PyTypeObject *) type)->tp_bases)) {
auto base_type = (PyTypeObject *) base.ptr();
if (base_type->tp_init == default_init) {
auto message = "%.200s has no __init__ and cannot be used as a base class from Python";
PyErr_Format(PyExc_TypeError, message, base_type->tp_name);
Py_DECREF(type);
return nullptr;
}
}
#endif
return type;
} Any thoughts on this, @virtuald? Side note: (the reason I'm surprised this works) When creating |
@rwgk Then what exactly is the point? You've also ignored my point that this exception is meant for users of pybind11-created libraries, rather than for developers of pybind11 libraries. IMO,
|
To remove the potential for doubt: someone looking at the current error message may reasonably wonder if it could work if there is no My original question under #2152 was: do you know how much trouble it is to check if the base class implements I see Dustin spent quite a bit of effort, but now seeing his work, and his "could be bad" remarks, my feeling is it's too much risk trying to be smarter here than tweaking the error message, especially if it doesn't work with PyPy. |
I think adjusting the error message would be sufficient if there's no consensus about the danger posed by these changes. @YannickJadoul I really like the idea of raising an exception on subclassing, but still suffers from the same problem of depending on the instance base of internals staying consistent. |
But that's the problem, there is an Next to that, that doesn't answer my comment on whom this exception is meant for. This is not an exception that (exclusively) ends up with pybind11 users/developers of a library, but with the users of a library made with pybind11. I appreciate that in your/Google's case, you were both the writer of the pybind11 bindings, as well as the user trying to subclass it. But this is very different in my use case (a library consumed by mostly exclusively Python users). So, separating concerns and taking the perspective of a library user, this exception should not allude to the implementation detail that there is no If the exception needs to be adapted, I think something like (personally, I still don't really see the point. A library user forgets to call |
@virtuald Yeah, I quite like the fact that you managed to check it (and there're more places where PyPy acts slightly different). The question is how consistently |
If the base class doesn't have a constructor, tell the user that it cannot be inherited from. Caveats: * Doesn't work on PyPy * If the default instance base was no longer a PyHeapTypeObject this could crash. * If pybind11_object_init changed in the future, and multiple pybind11 modules are present, get_internals might return an old version of the function
49aa611
to
e1e8d7a
Compare
Hi @virtuald (and @YannickJadoul), I believe we will need Wenzel's approval if we have this problem of depending on the instance base of internals staying consistent. — I still believe it's more trouble than it's worth. |
For the solution in this PR, yes, I agree! |
@virtuald, actually, is all this https://en.cppreference.com/w/cpp/language/inline says:
|
Presumably prior to C++17 the hocus pocus would be required? ... and anyways, it didn't work without the hocus pocus. I'd be happy to be proved wrong though. |
Hmmm, that part wasn't set as C++17 only, but I didn't try for myself. Dang, thought it would clean things up a bit :-( |
Improvement of #2152 as suggested by @rwgk and @YannickJadoul