What happens if the decorator factory has `__decoration_call__` and the
object it returns only has `__call__`? I presume you get this:

    func = decorator.__decoration_call__("spam this", "func").__call__(func)

And let's not forget the other two combinations:

    func = decorator.__decoration_call__("spam this", "func").__decoration_call__(func, "func")
    func = decorator.__call__("spam this").__call__(func)

The last one is, of course, the current behaviour for a decorator
factory.

The bottom line here is that is you have plain, unadorned decorator:

    @decorator

there are two possible behaviours and no obvious way to tell which one
is used, short of digging into the implementation. But if you have a
decorarator factory:

    @factory(*args, **kwargs)

there are now four possible behaviours. And anyone brave enough to use a
double-barrelled factory-factory

    @factory(*args, **kwargs)(*more_args)

will be faced with eight possible combinations.

I'm not the OP, but the way I understand the proposal __decoration_call__ is only invoked when you actually use an object to decorate something. That means that a decorator factory will just invoke __call__ as normal, because it's nothing but a convenient way to generate a decorator. It is not itself a decorator, nor is it used to actually decorate anything. To illustrate this point we can separate it out across several lines:

@factory("foo")
def bar():
    pass

Can be rewritten as:

decorator = factory("foo")

@decorator
def bar():
    pass

So __decorator_call__ will only be invoked on the object that gets returned from `factory("foo")`, not on `factory`.


It seems to me that this proposal means that we can't even tell which of
the two protocols (classic decoration, or new `__decoration_call__`
style decoration) without digging into the implementation of the
decorator.

To be precise, the problem here as reader isn't so much the fact that I
don't know whether the object is called using the `__call__` protocol or
the new-style `__decorator_call__` protocol, but the fact that I can't
tell whether the calls will involve the name being passed or not.


The OP mentioned a default implementation for __decoration_call__ of:

def  __decoration_call__(self, func, by_name):
    if func is None:
        return self(by_name)
    return self(func)

Such that you can assume that the decorator will *always* receive the name, but may choose to discard it and not make use of it if it doesn't implement the __decoration_call__ interface and instead opts to use default implementation which falls back on __call__.

For decorated functions the name can always be pulled out of the function object as normal even when using __call__, but to make use of the name in a decorated assignment statement the decorator would have to override __decoration_call__.


At this point I will say that I may be putting words into OPs mouth, and would be happy to be corrected if I've misunderstood.


One final point I've just thought of is that Ricky suggested that when no value is assigned to a name that the object reference be `None`. But I don't think that works, because it becomes indistinguishable from when `None` is explicitly assigned. We would need some sentinel value instead of `None` to remove ambiguity in this situation:


from somewhere import NOTSET

@decorate
foo: int

def __decoration_call__(self, obj, names, annotation):
    print(obj is None)    # False
    print(obj is NOTSET)  # True

@decorate
foo: int = None


def __decoration_call__(self, obj, names, annotation):
    print(obj is None)    # True
    print(obj is NOTSET)  # False


On Thu, May 27, 2021 at 3:25 PM Steven D'Aprano <steve@pearwood.info> wrote:
On Wed, May 26, 2021 at 12:43:48PM -0400, Ricky Teachey wrote:

[...]
> These two ideas of a decorator syntax result are not the same:
>
> RESULT A: function decorator
> # func = decorator("spam")(func)
>
> RESULT B: variable decorator
> # name = decorator("spam")("name")
>
> ...because func is passed as an object, but "name" a string representing
> the name of the object. Two very different things.

Ricky, it's not clear to me whether you are proposing the above RESULT A
and RESULT B as an *alternative* to the "variable decorator" proposal,
or if you have just misunderstood it. The current variable decorator
proposal on the table is for this:

    @decorator(spam) name
    # --> name = decorator("name", spam)

rather than what you wrote:

    # name = decorator("spam")("name")

So I can't tell whether the difference between your version and the OPs
is a bug or a feature :-)


> For this reason I think I would agree even more so that the differences in
> the decorator behavior would be an extremely significant point of confusion.
[...]
> Maybe employment of decorator syntax could OPTIONALLY trigger a new dunder
> method-- here I'll just call it __decoration_call__-- with the signature:
>
> def  __decoration_call__(self, obj: Any, by_name: str) -> Any: ...

To be clear here, I think that your proposal is that this method is to
be looked up on the *decorator*, not the thing being decorated. Is that
correct?

In other words:

    @decorator
    class X: ...  # or a function, or something else

it is *decorator*, not X, that is checked for a `__decoration_call__`
method.

Correct?


> My idea is to optionally allow any callable object to write a
> __decoration_call__ method that gets called in lieu of the __call__ method
> when the callable object is employed using decorator syntax. When this
> happens, the decorated named is supplied- not counting self- as the first
> argument (e.g., by_name), which contains the str value of the name the
> decorator was applied to.

In current Python, the only objects which can be decorated with the @
syntax are callable functions and classes. So it is ambiguous to talk
about "any callable object" without stating whether it is the decorator
or the thing being decorated.


> In actuality, unless I'm wrong (I might be; not an expert) current
> decorator syntax is really sugar for:
>
> def func(): ...
> func = decorator.__call__("spam this").__call__(func)

Roughly speaking, that would correspond to

    @decorator("spam this")
    def func():
        ...


If we have a bare decorator, we have this:

    @decorator
    def func():
        ...

    # --> func = decorator.__call__(func)


> My proposal is to make it such that:
>
> @decorator
> def func(): ...
>
> ...*can result* in this:
>
> def func(): ...
> func = decorator.__decoration_call__( func, "func")


Okay. Without reading the source code, does this code snippet use the
old `__call__` protocol or the new `__decoration_call__` protocol?


    @flambé
    class Banana_Surprise:
        pass


It seems to me that this proposal means that we can't even tell which of
the two protocols (classic decoration, or new `__decoration_call__`
style decoration) without digging into the implementation of the
decorator.

To be precise, the problem here as reader isn't so much the fact that I
don't know whether the object is called using the `__call__` protocol or
the new-style `__decorator_call__` protocol, but the fact that I can't
tell whether the calls will involve the name being passed or not.

This is because the name is being *implicitly* passed, in a way that is
unclear whether or not it will be passed.

I just don't know whether or not the decorator `flambé` receives the
name or not.


> And also so that this:
>
> @decorator("spam this")
> def func(): ...
>
> ...*can result* in this:
>
> def func(): ...
> func = decorator.__call__("spam this").__decoration_call__(func, "func")


What happens if the decorator factory has `__decoration_call__` and the
object it returns only has `__call__`? I presume you get this:

    func = decorator.__decoration_call__("spam this", "func").__call__(func)

And let's not forget the other two combinations:

    func = decorator.__decoration_call__("spam this", "func").__decoration_call__(func, "func")
    func = decorator.__call__("spam this").__call__(func)

The last one is, of course, the current behaviour for a decorator
factory.

The bottom line here is that is you have plain, unadorned decorator:

    @decorator

there are two possible behaviours and no obvious way to tell which one
is used, short of digging into the implementation. But if you have a
decorarator factory:

    @factory(*args, **kwargs)

there are now four possible behaviours. And anyone brave enough to use a
double-barrelled factory-factory

    @factory(*args, **kwargs)(*more_args)

will be faced with eight possible combinations.


And at this point, I'm afraid I have run out of steam to respond further
to this proposal. Sorry.


--
Steve
_______________________________________________
Python-ideas mailing list -- python-ideas@python.org
To unsubscribe send an email to python-ideas-leave@python.org
https://mail.python.org/mailman3/lists/python-ideas.python.org/
Message archived at https://mail.python.org/archives/list/python-ideas@python.org/message/GZQ2RTWVNW6KC2REISB34TLRH45ZJIMK/
Code of Conduct: http://python.org/psf/codeofconduct/