
On 29 September 2017 at 08:04, Ivan Levkivskyi <levkivskyi@gmail.com> wrote:
On 28 September 2017 at 08:27, Nick Coghlan <ncoghlan@gmail.com> wrote:
On 27 September 2017 at 19:28, Ivan Levkivskyi <levkivskyi@gmail.com> wrote:
If an object that is not a class object appears in the bases of a class definition, the ``__subclass_base__`` is searched on it. If found, it is called with the original tuple of bases as an argument. If the result of the call is not ``None``, then it is substituted instead of this object. Otherwise (if the result is ``None``), the base is just removed. This is necessary to avoid inconsistent MRO errors, that are currently prevented by manipulations in ``GenericMeta.__new__``. After creating the class, the original bases are saved in ``__orig_bases__`` (currently this is also done by the metaclass).
How would you feel about calling it "__mro_entry__", as a mnemonic for "the substitute entry to use instead of this object when calculating a subclass MRO"?
I don't have any preferences for the name, __mro_entry__ sounds equally OK to me.
I'd propose changing it then, as searching for "Python mro entry" is likely to get people to the right place faster than searching for "Python subclass base".
I think the other thing that needs to be clarified is whether or not the actual metaclass can expect to receive an already-resolved sequence of MRO entries as its list of bases, or if it will need to repeat the base resolution process executed while figuring out the metaclass.
There are three points for discussion here:
1) It is necessary to make the bases resolution soon, before the metaclass is calculated. This is why I do this at the beginning of __build_class__ in the reference implementation.
Indeed.
2) Do we need to update type.__new__ to be able to accept non-classes as bases? I think no. One might be a bit surprised that
class C(Iterable[int]): pass
works, but
type('C', (Iterable[int],), {})
fails with a metaclass conflict, but I think it is natural that static typing and dynamic class creation should not be used together. I propose to update ``type.__new__`` to just give a better error message explaining this.
+1 from me, since that avoids ever resolving the list of bases twice.
3) Do we need to update types.new_class and types.prepare_class? Here I am not sure. These functions are rather utility functions and are designed to mimic in Python what __build_class__ does in C. I think we might add types._update_bases that does the same as its C counterpart. Then we can update types.new_class and types.prepare_class like you proposed, this will preserve their current API while types.new_class will match behaviour of __build_class__
Your suggestion for `types.__new__` gave me a different idea: what if `types.prepare_class` *also* just raised an error when given a non-class as a nominal base class? Then if we added `types.resolve_bases` as a public API, a full reimplementation of `types.new_class` would now look like: resolved_bases = types.resolve_bases(bases) mcl, ns, updated_kwds = types.prepare_class(name, resolved_bases, kwds) exec_body(ns) ns["__orig_bases__"] = bases mcl(name, resolved_bases, ns, **updated_kwds) That way, `types.new_class` would transparently switch to the new behaviour, while clients of any other dynamic type creation API could do their own base class resolution, even if the type creation API they were using didn't implicitly support MRO entry resolution. Cheers, Nick. -- Nick Coghlan | ncoghlan@gmail.com | Brisbane, Australia