On Thu, May 27, 2021 at 10:25 AM 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.

No, I understood the OP's proposal perfectly. I was agreeing with you implicitly when you previously said the inconsistency between the OP's proposal and current decorator is a problem:

<STEVE WROTE>:
On the other hand, it would be terribly confusing if the same syntax:
    @decorate
had radically different meaning depending on whether it was followed by
a class/function or a bare name

And furthermore, it is even a bigger problem than you explicitly made it out to be because of passing and object vs. passing a string representing the name of an object.

So I was not illustrating the OP's proposal, but showing (but not proposing) a modified version of it that acts more like decorators today, so that we would have this:

# NOT THE OP's PROPOSAL -- more consistent with decorators today, but not as quickly useful on its own
@decorator("spam this") var
#  decorator("spam this")("var")

..rather than this:

# OP's PROPOSAL
@decorator("spam this") var
#  decorator("spam this", "var")
 
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 :-)

Yes, I am not proposing this behavior or saying it was the OP's, just illustrating to agree with you that it is so different from current decorator behavior, I don't think it should be considered (even though I sort of like how it looks).
 
> 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?

Yes, it is looked up on the decorator, i.e., the result of the expression immediately to the right of the @ symbol. So it is only used when using decorator syntax (i.e., the @ symbol).
 
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?

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

I am sorry, I thought I made this clear early on in the proposal. yes, the decorator is the thing. More precisely, the result of the expression to the right of the @ symbol.

By "any callable", I had in mind how any callable can be used as a decorator (so long as it isn't expecting more than 1 positional argument and no required kwd arguments). But really, the result any expression can be a decorator now (as of 3.10 I believe?), thought you might get an error:

@1/2
def func(): ...
# TypeError: 'float' object is not callable
 
> 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():
        ...

Yes, I had written exactly that above the quoted part:

So, for any existing callable with the name decorator, this:
@decorator("spam this")
def func(): ...
...continues to mean this, just as it does today:
def func(): ...
func = decorator("spam this")(func)

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

Invoking @flambé causes:

flambé.__decoration_call__(Banana_Surprise, "Banana_Surprise")

...to be used, if flambé has that method.
 
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.

HMMM.... to be totally honest I had not considered the problem this idea poses for introspection and documentation. But after giving it some thought, I don't see it as a significant problem.

Yes, you would have to read the source code of flambé, or consult help/introspection tools that know about the new protocol, to know whether a __decoration_call__ method will be used.

And introspection/help tools would need to present both the normal call signature:

flambé()  <--->   flambé.__call__(self, spam, /, eggs, *args, cheese, **kwargs)

....and the decoration call signature, which is intended to be FIXED at two position args:

@flambé  <--->   flambé.__decoration_call__(self, func, by_name)

Of course, anyone could make the __decoration_call__ signature anything they wanted:

@flambé  <--->   flambé.__decoration_call__(self, func, /, by_name, *chaos, pain="why?", **suffering)

I do see that as a potential problem now; but I think it's a minor problem. Help and tutorial guides/files would have to teach people that there is potentially a different calling signature when decorator syntax is used, just as they teach that other syntactical symbols call different dunder methods when they are used:

a += 1
a + 1

But I think the flexibility we will get could be worth this didactic overhead.
 
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.

If you wrote the decorator, you know.

If you didn't, why does it matter if it is being passed? Why do you need to know that?

Some descriptor have __set_name__ methods, and others don't. We use them very happily in both cases.

The callable function you are using as a decorator has a job; presumably you know what the job is or you'd not be using the callable. Do you really have to concern yourself with HOW it does it, unless you are the maintainer, or tracking down a bug inside the implementation of the decorator? At which point you'll have to read the source code anyway.
 
> 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)

No. I am NOT-- repeat, NOT-- proposing a change to the behavior of the call operator, ().

The call operator, (), ALWAYS does this:

decorator_factory()  <-->   decorator_factory.__call__()

This is true regardless of whether that call is prepended by the @ symbole. Clear?

Under NO circumstances does:

decorator_factory("spam this")

...result in this:

decorator_factory.__decoration_call__("spam this")

No. ONLY prepending the @ symbol at the beginning of a line invokes __decoration_call__, and it invokes it on the result of the expression immediately to the right, just as it does today:

@decorator_factory()  <-->   decorator_factory().__decoration_call__
@( decorator := decorator_factory()  )  <-->   ( decorator := decorator_factory()  ) .__decoration_call__  
@tricky_decorator + 1 / 2 "lala" ^ None  <-->   ( tricky_decorator + 1 / 2 "lala" ^ None ).__decoration_call__  

The object that is returned by the factory object is the decorator. NOT the decorator factory.

These code blocks result in identical behavior for func:

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

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

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

The object returned by the expression to the right of the @ symbol is not in any way affected by the proposal. Everything happens as it does today. Zero changes.
 
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.

I said it above already but I don't see why this is such a problem. Presumably you are using this callable as a decorator because you are confident to some degree it will do the job it is supposed to do. If it needs the by_name argument to do that job, it will make use of the __decoration_call__ dunder. If it doesn't need it, it won't.
 
But if you have a
decorarator factory:

    @factory(*args, **kwargs)

there are now four possible behaviours.

No, as I explained above, that isn't the case. The decorator object returned by the factory call has two possible behaviors, yes. And yes, understand which happens, you have to look at how it is implemented.
 
And anyone brave enough to use a
double-barrelled factory-factory

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

will be faced with eight possible combinations.

No; it remains two:

factory.__call__(*args, **kwargs) .__call__ (*more_args) .__decoration_call__

OR, the default:

factory.__call__(*args, **kwargs) .__call__ (*more_args) .__call__  
 
And at this point, I'm afraid I have run out of steam to respond further
to this proposal. Sorry.


--
Steve


I don't blame you. The version of my proposal you had in mind was terrible; sorry I did not explain it so well the first time. I hope the version I actually have in mind will be better received.

---
Ricky.

"I've never met a Kentucky man who wasn't either thinking about going home or actually going home." - Happy Chandler