-
-
Notifications
You must be signed in to change notification settings - Fork 31.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
PEP 3115 compliant dynamic class creation #58793
Comments
As Nick Coghlan proposed [1, 2], there should be a way to dynamically create classes, which handles metaclasses correctly (see also bpo-1294232). 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:
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:
_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__.)
[1] http://mail.python.org/pipermail/python-dev/2011-April/110874.html |
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.) 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... |
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)) |
Fair enough. |
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. |
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). |
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. |
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:
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 |
Implementing in pure Python seems to have a lot of pros and no con to me. |
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: The tests are mostly the same as in my previous patch. |
New changeset befd56673c80 by Nick Coghlan in branch 'default': |
Great doc patch. I think it would be worthwhile to backport it. |
Note: these values reflect the state of the issue at the time it was migrated and might not reflect the current state.
Show more details
GitHub fields:
bugs.python.org fields:
The text was updated successfully, but these errors were encountered: