[Python-ideas] Add a more disciplined ways of registering ABCs

אלעזר elazarg at gmail.com
Fri Sep 8 11:05:28 EDT 2017


Hi all,

tl;dr: I propose adding a `register()` decorator, to be used like this:

    @abc.register(Abc1, Abc2)
    class D:
        ...

For preexisting classes I propose adding a magic static variable
`__registered__`, to be handled by ABCMeta:

    class Abc1(metaclass=ABCMeta):
        __registered__ = [D]

Both forms has the effects of calling ABCMeta.register with the
corresponding parameters.

Explanation:

It is possible to register an abstract base class B for preexisting classes
A using B.register(A). This form is very flexible - probably too flexible,
since the operation changes the type of A, and doing it dynamically makes
it difficult for type checker (and human beings) to analyze.

But what about new classes? Registering a new class is possible using
B.register as a decorator, since it already its argument:

    @Iterable.register
    @Sequence.register
    class A: pass

However, due to restrictions on the expressions allowed inside a decorator,
this would not work with generic ABCs, which are common and heavily used in
type-checked projects:

    >>> @Iterable[str].register
      File "<stdin>", line 1
        @Iterable[str].register
                        ^
    SyntaxError: invalid syntax

While it might be possible to infer the argument, it is not always the
case, and adds additional unnecessary burden to type-checker and to the
reader; discouraging its use. This kind of restrictions may also prevent
some other conceivable forms, such as `@type(X).register`.

Additionally, `abc.register()` is also easier to analyze as a "syntactic
construct" from the point of view the type checker, similar to the way
checkers handle some other constructs such as `enum()`, `cast()` and others.

Finally, there's uninformative repetition of `register` for multiple ABCs,
and waste of vertical screen space.

Q: Why not subclass B?
A: Since it forces the metaclass of A to be (a subclass of) ABCMeta, which
can cause inconsistency in metaclass structure for derived classes. This
issue can surface when using Enum (whose metaclass is EnumMeta):

    >>> class A(Iterable, Enum): pass
    ...
    Traceback (most recent call last):
      File "<stdin>", line 1, in <module>
    TypeError: metaclass conflict: the metaclass of a derived class must be
a (non-strict) subclass of the metaclasses of all its bases

This causes problems in typeshed, since it uses inheritance to register
`str` as a `Sequence[str]`, for example. As a result, it affects users of
mypy trying to define `class A(str, Enum): ...`

Q: Why not add to the typing module?
A: Because registering an abstract base class has runtime meaning, such as
the result of `isinstance` checks.

Q: Why not add to the standard library? why not some 3rd party module?
A:
1. Because dynamic usage should be discouraged, similarly to
monkey-patching. Note that, like monkey-patching, this has runtime
implications which cause "actions from distance".
2. Because it should be used by typeshed and mypy.

The implementation of register() is straightforward:

    def register(*abcs):
        def inner(cls):
            for b in abcs:
                b.register(cls)
            return cls
        return inner


Thanks,
Elazar
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <http://mail.python.org/pipermail/python-ideas/attachments/20170908/73c33c54/attachment.html>


More information about the Python-ideas mailing list