Skip to content
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

Closed
durban mannequin opened this issue Apr 16, 2012 · 12 comments
Closed

PEP 3115 compliant dynamic class creation #58793

durban mannequin opened this issue Apr 16, 2012 · 12 comments
Assignees
Labels
extension-modules C modules in the Modules dir interpreter-core (Objects, Python, Grammar, and Parser dirs) stdlib Python modules in the Lib dir type-feature A feature request or enhancement

Comments

@durban
Copy link
Mannequin

durban mannequin commented Apr 16, 2012

BPO 14588
Nosy @ncoghlan, @merwok, @bitdancer, @durban
Files
  • operator_build_class.patch: operator.build_class - 1st patch
  • operator_build_class_2.patch: operator.build_class - 2nd patch with more tests
  • operator_build_class_3.patch: operator.build_class - 3rd patch with small fixes
  • types_new_class.patch: types.new_class - 1st patch
  • 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:

    assignee = 'https://github.com/ncoghlan'
    closed_at = <Date 2012-05-19.16:34:27.110>
    created_at = <Date 2012-04-16.00:23:18.232>
    labels = ['extension-modules', 'interpreter-core', 'type-feature', 'library']
    title = 'PEP 3115 compliant dynamic class creation'
    updated_at = <Date 2012-05-19.20:57:00.952>
    user = 'https://github.com/durban'

    bugs.python.org fields:

    activity = <Date 2012-05-19.20:57:00.952>
    actor = 'eric.araujo'
    assignee = 'ncoghlan'
    closed = True
    closed_date = <Date 2012-05-19.16:34:27.110>
    closer = 'python-dev'
    components = ['Extension Modules', 'Interpreter Core', 'Library (Lib)']
    creation = <Date 2012-04-16.00:23:18.232>
    creator = 'daniel.urban'
    dependencies = []
    files = ['25231', '25263', '25292', '25546']
    hgrepos = []
    issue_num = 14588
    keywords = ['patch']
    message_count = 12.0
    messages = ['158382', '158660', '158673', '158734', '158792', '158866', '160134', '160394', '160409', '160466', '161135', '161157']
    nosy_count = 5.0
    nosy_names = ['ncoghlan', 'eric.araujo', 'r.david.murray', 'daniel.urban', 'python-dev']
    pr_nums = []
    priority = 'normal'
    resolution = 'fixed'
    stage = 'resolved'
    status = 'closed'
    superseder = None
    type = 'enhancement'
    url = 'https://bugs.python.org/issue14588'
    versions = ['Python 3.3']

    @durban
    Copy link
    Mannequin Author

    durban mannequin commented Apr 16, 2012

    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:

    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__.)

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

    2. 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

    @durban durban mannequin added interpreter-core (Objects, Python, Grammar, and Parser dirs) extension-modules C modules in the Modules dir type-feature A feature request or enhancement labels Apr 16, 2012
    @durban
    Copy link
    Mannequin Author

    durban mannequin commented Apr 18, 2012

    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...

    @ncoghlan
    Copy link
    Contributor

    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))

    @durban
    Copy link
    Mannequin Author

    durban mannequin commented Apr 19, 2012

    Fair enough.

    @ncoghlan
    Copy link
    Contributor

    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.

    @durban
    Copy link
    Mannequin Author

    durban mannequin commented Apr 20, 2012

    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).

    @ncoghlan
    Copy link
    Contributor

    ncoghlan commented May 7, 2012

    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.

    @ncoghlan
    Copy link
    Contributor

    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

    @merwok
    Copy link
    Member

    merwok commented May 11, 2012

    Implementing in pure Python seems to have a lot of pros and no con to me.

    @durban
    Copy link
    Mannequin Author

    durban mannequin commented May 12, 2012

    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.

    @durban durban mannequin added the stdlib Python modules in the Lib dir label May 12, 2012
    @python-dev
    Copy link
    Mannequin

    python-dev mannequin commented May 19, 2012

    New changeset befd56673c80 by Nick Coghlan in branch 'default':
    Close bpo-14588: added a PEP-3115 compliant dynamic type creation mechanism
    http://hg.python.org/cpython/rev/befd56673c80

    @python-dev python-dev mannequin closed this as completed May 19, 2012
    @merwok
    Copy link
    Member

    merwok commented May 19, 2012

    Great doc patch. I think it would be worthwhile to backport it.

    @ezio-melotti ezio-melotti transferred this issue from another repository Apr 10, 2022
    Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
    Labels
    extension-modules C modules in the Modules dir interpreter-core (Objects, Python, Grammar, and Parser dirs) stdlib Python modules in the Lib dir type-feature A feature request or enhancement
    Projects
    None yet
    Development

    No branches or pull requests

    2 participants