Skip to content

[QUESTION] Defining and using metaclasses with pybind11 #2696

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
jbms opened this issue Nov 26, 2020 · 7 comments
Open

[QUESTION] Defining and using metaclasses with pybind11 #2696

jbms opened this issue Nov 26, 2020 · 7 comments

Comments

@jbms
Copy link
Contributor

jbms commented Nov 26, 2020

I am aware of the pybind11::metaclass option that can be passed to pybind11::class_, but I can't find any documentation on how it is supposed to be used. There is a single test case here:

// test_metaclass_override

but it isn't clear what that case is showing, and I haven't been able to find a single other example of code that uses pybind11::metaclass.

What I'd like to accomplish is to make __class_getitem__ work on Python < 3.7, equivalent to the following pure python code:

class Parent(type):

    def __getitem__(self, key):
        return self.__class_getitem__(key)


class Child(metaclass=Parent):

    @classmethod
    def __class_getitem__(cls, key):
        return 'got key: %r' % (key,)

assert Child[1] == 'got key: 1'

Note that in Python >= 3.7, this example also works if we eliminate Parent and just use type as the metaclass of Child.

I'd like to accomplish this same thing, where both Parent and Child are defined using pybind11 rather than pure Python.

My initial attempt was:

  struct Parent {};
  struct Child {};

  py::class_<Parent> cls_parent(
      m, "Parent",
      py::handle(reinterpret_cast<PyObject*>(
          pybind11::detail::get_internals().default_metaclass));
  cls_parent.def("__getitem__", [](Parent& self, py::object key) {
    return py::cast(&self).attr("__class_getitem__")(key);
  });

  py::class_<Child> cls_child(m, "Child", py::metaclass(cls_parent));
  cls_child.def_static("__class_getitem__",
                       [](std::string key) { return "got key: " + key; });

but that crashes while creating cls_parent: due to the t_size >= b_size condition in the extra_ivars function in typeobject.c.

@jbms
Copy link
Contributor Author

jbms commented Nov 29, 2020

I managed to get it working using plain CPython APIs:

PyTypeObject* GetClassGetitemMetaclass() {
#if PY_VERSION_HEX < 0x030700000
  // Polyfill __class_getitem__ support for Python < 3.7
  static auto* metaclass = [] {
    PyTypeObject* base_metaclass =
        pybind11::detail::get_internals().default_metaclass;
    PyType_Slot slots[] = {
        {Py_tp_base, base_metaclass},
        {Py_mp_subscript,
         (void*)+[](PyObject* self, PyObject* arg) -> PyObject* {
           auto method = py::reinterpret_steal<py::object>(
               PyObject_GetAttrString(self, "__class_getitem__"));
           if (!method.ptr()) return nullptr;
           return PyObject_CallFunctionObjArgs(method.ptr(), arg, nullptr);
         }},
        {0},
    };
    PyType_Spec spec = {};
    spec.name = "_Metaclass";
    spec.basicsize = base_metaclass->tp_basicsize;
    spec.flags = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE;
    spec.slots = slots;
    PyTypeObject* metaclass = (PyTypeObject*)PyType_FromSpec(&spec);
    if (!metaclass) throw py::error_already_set();
    return metaclass;
  }();
  return metaclass;
#else  // Python version >= 3.7 supports __class_getitem__ natively.
  return nullptr;
#endif
}

Is it possible to define metaclasses with pybind11 directly, though?

In this case it ended up being pretty easy to define with the CPython API directly so I suppose there isn't really a need for that.

@EricCousineau-TRI
Copy link
Collaborator

EricCousineau-TRI commented Dec 23, 2020

FWIW I'm briefly trying out something semi-related for #2332; at the moment, I'm just trying to write it in pure Python, but may fall back to CPython API like you tried.

Can I ask how you passed the result of GetClassGetitemMetaclass() to your py::class_? Did you just cast it to py::handle?

EDIT: I think that's about what I did, but for py::object:

reinterpret_borrow<py::object>(py::reinterpret_cast<PyObject*>(py::detail::get_internals().default_metaclass))

@jbms
Copy link
Contributor Author

jbms commented May 15, 2021

@nicola-gigante
Copy link

Hi! I'm trying to use py::metaclass but it doesn't work for me. This question seems to ask what I need but there is no general answer.

For what I need to do, you can look at this SO question. In a few words: I'm trying to define __instancecheck__ to overload isinstance() but I cannot define a custom metaclass.

How is it supposed to be done?

@jbms
Copy link
Contributor Author

jbms commented Jul 3, 2022

I don't believe there is any built-in support in pybind11 for defining a metaclass --- instead you have to use the Python C API directly to define the metaclass, as I did here:
https://github.com/google/tensorstore/blob/24053fc2492ea958109ade321bdc8456688167f6/python/tensorstore/dim_expression.cc#L350

@nicola-gigante
Copy link

nicola-gigante commented Jul 4, 2022

I see, thanks. I'm not familiar at all with the C API. Can you help me understand your code and maybe adapt it to __instancecheck__? Also, I see you define a method contextually to the class definition. Is it possible to create the metaclass using the C API but then obtaining a py::handle object and defining the method using pybind11? Also see this discussion.

@ezyang
Copy link

ezyang commented Oct 20, 2022

On the metaclass, you'd have to define tp_methods to have an instancecheck definition, then it should work

pytorchmergebot pushed a commit to pytorch/pytorch that referenced this issue Nov 15, 2022
Summary:
- Customize the metaclass of `torch.distributed.distributed_c10d.ReduceOp` for the sake of custom `__instancecheck__`
- Add `copy.copy`, `copy.deepcopy`, and `pickle` support with tests

Rel:
- #81272
- #84243
- #87191
- #87303
- #87555

Ref:
- pybind/pybind11#2696

Pull Request resolved: #88275
Approved by: https://github.com/wanchaol
kulinseth pushed a commit to kulinseth/pytorch that referenced this issue Dec 10, 2022
)

Summary:
- Customize the metaclass of `torch.distributed.distributed_c10d.ReduceOp` for the sake of custom `__instancecheck__`
- Add `copy.copy`, `copy.deepcopy`, and `pickle` support with tests

Rel:
- pytorch#81272
- pytorch#84243
- pytorch#87191
- pytorch#87303
- pytorch#87555

Ref:
- pybind/pybind11#2696

Pull Request resolved: pytorch#88275
Approved by: https://github.com/wanchaol
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

4 participants