classification
Title: PEP 3115 compliant dynamic class creation
Type: enhancement Stage: resolved
Components: Extension Modules, Interpreter Core, Library (Lib) Versions: Python 3.3
process
Status: closed Resolution: fixed
Dependencies: Superseder:
Assigned To: ncoghlan Nosy List: daniel.urban, eric.araujo, ncoghlan, python-dev, r.david.murray
Priority: normal Keywords: patch

Created on 2012-04-16 00:23 by daniel.urban, last changed 2012-05-19 20:57 by eric.araujo. This issue is now closed.

Files
File name Uploaded Description Edit
operator_build_class.patch daniel.urban, 2012-04-16 00:23 operator.build_class - 1st patch review
operator_build_class_2.patch daniel.urban, 2012-04-18 20:50 operator.build_class - 2nd patch with more tests review
operator_build_class_3.patch daniel.urban, 2012-04-20 18:10 operator.build_class - 3rd patch with small fixes review
types_new_class.patch daniel.urban, 2012-05-12 09:49 types.new_class - 1st patch review
Messages (12)
msg158382 - (view) Author: Daniel Urban (daniel.urban) * Date: 2012-04-16 00:23
As Nick Coghlan proposed [1, 2], there should be a way to dynamically create classes, which handles metaclasses correctly (see also issue1294232).

Here is my first attempt at creating an operator.build_class method. It only includes very simple tests and no documentation, but I will write them if needed.

With this patch there are two functions for creating a class object:
1. __build_class__ (no change)
2. operator.build_class(name, bases=(), kwds=None, eval_body=None): finds the correct metaclass and calls its __prepare__. If eval_body is given, calls it with the namespace returned by __prepare__. Then calls the correct metaclass, and returns the created class object.

Both of these functions (after parsing their arguments) call _PyType_BuildClass, a new C API function. The first argument of this function is a callable, that will be called with the namespace returned by __prepare__ (it also can be NULL, in that case nothing will be called). __build_class__ passes the function that is the body of the class statement. operator.build_class passes the callable given by the user (or NULL, if the user didn't pass the eval_body argument). The implementation of _PyType_BuildClass is approximately the following:

def _PyType_BuildClass(func=None, name, bases, kwds={}):
    meta = kwds.pop('metaclass', None)
    if meta is None:
        if not bases:
            meta = type
        else:
            meta = type(bases[0])
    ns, meta = prepare_namespace(name, meta, bases, kwds)
    if func is not None:
        func(ns)
    return meta(name, bases, ns, kwds)

(Actually the return value of the func is used if it's a cell object. I'm not sure, why and when this is needed, this code comes from __build_class__.)

The changes are in the following files:

1. object.h: the exported function is _PyType_BuildClass instead of _PyType_CalculateMetaclass (that doesn't need to be in the include file anymore).

2. operator.c: the build_class method checks its arguments, then calls _PyType_BuildClass.

3. typeobject.c:

_PyType_CalculateMetaclass is renamed to calculate_metaclass, because now it is only called from this file.

prepare_namespace calls calculate_metaclass to determine the correct metaclass, then calls its __prepare__ method. (This code is moved here mostly from __build_class__). It also passes back the correct metaclass to its caller.

_PyType_BuildClass gets the starting metaclass from its arguments. Then it calls prepare_namespace to get the namespace and the correct metaclass. If it received a non-NULL first argument (the function that is the class body or the eval_body argument of operator.build_class), then calls it, passing the namespace. Then it calls the correct metaclass. (Most of this code is also from __build_class__.)

4. bltinmodule.c: builtin___build_class__ now only parses its arguments, and simply calls _PyType_BuildClass.

5. test_operator.py: a simple test for operator.build_class


[1] http://mail.python.org/pipermail/python-dev/2011-April/110874.html
[2] http://mail.python.org/pipermail/python-dev/2012-April/118732.html
msg158660 - (view) Author: Daniel Urban (daniel.urban) * Date: 2012-04-18 20:50
I've attached a patch with more tests. I simply copied and modified the tests about metaclass calculation and __prepare__ in test_descr.py, to create the tested classes with operator.build_class (and not the class statement).

Although, there is one thing I'm not sure I like about the API in the current patch: the dictionary corresponding to the keyword arguments of the class statement cannot be passed as keyword arguments. For example, I can't write this:

   C = operator.build_class('C', (A, B), metaclass=MyMeta)

I have to write this:

   C = operator.build_class('C', (A, B), {'metaclass': MyMeta})

(The reason for this is that the eval_body argument is the last.)
What would you think about the following signature for build_class?

   build_class(name, bases=(), eval_body=None, **kwargs)

The fist 3 argument could be positional only, and all keyword arguments would go into the dict. A downside is that the user would have to explicitly pass None as the 3rd argument, if they don't need an eval_body, but need keyword-arguments. Also, the 'bases' and the keyword arguments wouldn't be close to each other as in the class statement...
msg158673 - (view) Author: Nick Coghlan (ncoghlan) * (Python committer) Date: 2012-04-18 22:09
I thought about that, and I'd prefer a dedicated dictionary to avoid questions of name conflicts.

Wrapping the keyword args in a dict() call is still pretty clean:

    C = operator.build_class('C', (A, B), dict(metaclass=MyMeta))
msg158734 - (view) Author: Daniel Urban (daniel.urban) * Date: 2012-04-19 15:50
Fair enough.
msg158792 - (view) Author: Nick Coghlan (ncoghlan) * (Python committer) Date: 2012-04-20 03:18
It occurs to me that, for naming consistency, the callback arg should be documented as "exec_body" rather than "eval_body".

I'll try to get to a proper patch review this weekend.
msg158866 - (view) Author: Daniel Urban (daniel.urban) * Date: 2012-04-20 18:10
I've attached the third patch with the eval_body -> exec_body change; explicitly passing the default (None) now also allowed. I also fixed a refleak (I think).
msg160134 - (view) Author: Nick Coghlan (ncoghlan) * (Python committer) Date: 2012-05-07 11:05
In going to add documentation for your patch, I realised the operator module is not the right place for this.

The "types" module actually seems like the most appropriate home, but that will require adding a _types module to back it.

I'll post to python-dev to get additional feedback.
msg160394 - (view) Author: Nick Coghlan (ncoghlan) * (Python committer) Date: 2012-05-11 01:02
Based on the python-dev thread [1], the proposed name for this API is now "types.new_class()".

This parallels the existing imp.new_module() naming scheme and avoids various problems with the idea of using a static method on type itself (descriptors on type behave strangely, and the type namespace is accessible through *all* type objects, which would be weird in this case).

Since types is a Python module, we now have to choose between 3 implementation strategies:
- reimplement in pure Python (my preferred choice)
- implement in terms of __build_class__ (would work, but may not be portable to other implementations and/or serves as a de facto promotion of __build_class__ up to being part of the language specification)
- move Daniel's existing operator module based solution over to a new _types C extension module (again, may not help other implementations)

The reason I find the idea of a pure Python reimplementation appealing is that it can then serve as a cross-check for any other implementations implementing PEP 3115 for their class statements.

[1] http://mail.python.org/pipermail/python-dev/2012-May/119318.html
msg160409 - (view) Author: Éric Araujo (eric.araujo) * (Python committer) Date: 2012-05-11 11:41
Implementing in pure Python seems to have a lot of pros and no con to me.
msg160466 - (view) Author: Daniel Urban (daniel.urban) * Date: 2012-05-12 09:49
Here is my first attempt at creating a pure Python version of the operator.build_class function (in my previous patch) as types.new_class.

The three added functions (two private and one public) correspond to the following functions in my previous patch:
types.new_class -> operator.build_class
types._prepare_ns -> prepare_namespace in typeobject.c
types._calculate_mcls -> calculate_metaclass in typeobject.c (currently _PyType_CalculateMetaclass)
(In Python these functions are quite short, so they may be merged. But this separation may be better for documentation purposes...)

The tests are mostly the same as in my previous patch.
msg161135 - (view) Author: Roundup Robot (python-dev) Date: 2012-05-19 16:34
New changeset befd56673c80 by Nick Coghlan in branch 'default':
Close #14588: added a PEP 3115 compliant dynamic type creation mechanism
http://hg.python.org/cpython/rev/befd56673c80
msg161157 - (view) Author: Éric Araujo (eric.araujo) * (Python committer) Date: 2012-05-19 20:57
Great doc patch.  I think it would be worthwhile to backport it.
History
Date User Action Args
2012-05-19 20:57:00eric.araujosetmessages: + msg161157
2012-05-19 16:34:27python-devsetstatus: open -> closed

nosy: + python-dev
messages: + msg161135

resolution: fixed
stage: resolved
2012-05-12 09:49:17daniel.urbansetfiles: + types_new_class.patch

messages: + msg160466
components: + Library (Lib)
2012-05-11 11:41:50eric.araujosetmessages: + msg160409
2012-05-11 01:02:54ncoghlansetmessages: + msg160394
2012-05-07 11:05:21ncoghlansetmessages: + msg160134
2012-04-20 18:10:45daniel.urbansetfiles: + operator_build_class_3.patch

messages: + msg158866
2012-04-20 04:29:57r.david.murraysetnosy: + r.david.murray
2012-04-20 03:18:44ncoghlansetmessages: + msg158792
2012-04-19 15:50:51daniel.urbansetmessages: + msg158734
2012-04-18 22:09:21ncoghlansetmessages: + msg158673
2012-04-18 20:50:32daniel.urbansetfiles: + operator_build_class_2.patch

messages: + msg158660
2012-04-16 13:31:15eric.araujosetnosy: + eric.araujo
2012-04-16 11:40:08r.david.murraysetassignee: ncoghlan
2012-04-16 00:23:18daniel.urbancreate