On Wed, May 26, 2021 at 01:33:07PM +0200, Stéfane Fermigier wrote:
> Last point that I like in the decorator syntax: it's
>
> I can compose N different decorators and keep the intent obvious:
>
> @acl(READ, WRITE)
> @constraint(10 < _ < 100)
> @not_null
> @indexed
> @depends_on(whatever)
> @inject
> @...
> first_name: str
Hmm, that's a good point.
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.
--
Steve
....and previously Steve also said:
On Mon, May 24, 2021 at 06:36:47PM -0700, micro codery wrote:
> Basically this would add syntax to python that would transform
> @decorator("spam this") variable
> into
> variable = decorator("variable", "spam this")
That is confusingly different from decorator syntax in other contexts....
@decorator("spam this")
def func(): pass
# transformed to:
def func(): pass
func = decorator("spam this")(func)
...the critical difference is that the argument "spam this" should be
passed to the decorator *factory*, which then returns the actual
decorator that gets applied to the variable. (Or function/ class in the
case of regulator decorator syntax.)
Well, in actuality even if it were implemented the way you described, it is still going to be very different--- I might even say radically different.
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.
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.
This got me to thinking: what if
access to the variable name were provided by another means, and ONLY when the decorator syntax is employed?
So then I started to write this big long email and it kind of got out of hand. Hopefully it isn't a disaster.
FIRST LAYER: RICK'S (LIKELY HALF-BAKED) COUNTER PROPOSAL
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: ...
Before I describe what I intend by this, first I will stipulated that what I am proposing here should only be implemented so that the behavior of all currently existing decorators (i.e., all callables) would remain exactly as it does today.
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)
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.
Let's explain using code examples.
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)
My proposal is to make it such that:
@decorator
def func(): ...
...can result in this:
def func(): ...
func = decorator.__decoration_call__(
func, "func")
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")
I say "can result" because this has the following limitations:
- occurs only when the callable object is employed as a decorator
- occurs only when
__decoration_call__ exists (and if it doesn't, just revert back to the usual __call__(func_object, obj))
Here is an example to further illustrate the suggested behavior. It is not intended to demonstrate a useful example. You could write a callable object like this:
class Decorator:
def __call__(self, obj):
print("spam")
return obj
def
__decoration_call__(self
, obj,
by_name):
print("eggs")
print(by_name)
return obj
decorator = Decorator()
And the behavior would be like this:
def func(): ...
func = decorator(func)
# Console output:
# 'spam'
@decorator
def func(): ...
# Console output:
# 'eggs'
# 'func'
In order to preserve current behavior, the built-in <class 'function'> type would not grow its own new __decoration_call__ method. So in the case of existing normal functions, and also in the case of existing custom Callable objects, the fallback would be to just use __call__ as normal.
In this way, all of the proposal is optional. But as illustrated above, the new dunder can be implemented by any customized Callable class.
I think
__decoration_call__
by itself could provide a lot of interesting possibilities for existing class and function decoration. But my main motivation for this is as a different way of implementing the variable decorator idea proposed by Jeremiah.
SECOND LAYER: VARIABLE DECORATORS
With
__decoration_call__
in place, we could do a lot of interesting things if we take the additional step of broadening existing decorator syntax such that you can decorate not just functions and classes but also variables (similar the variable decorator proposal recently proposed by Jeremiah). But first let's define the variable decorator syntax behavior.
Using the same decorator object I have defined above, and borrowing Jeremiah's original example, the new syntax would transform this:
@decorator variable
...into this:
variable = decorator.__decoration_call__(None, "variable")
EXPLANATION OF None:
in the example above there is no assignment on the decorated line, and so
there is no object to pass to the dunder at that point
. Because of this, the first argument, which is usually the object being decorated, is supplied as None in the call to
__decoration_call__
. Only the name being assigned to the as-yet uncreated object exists.
And
the new syntax would transform this:
@decorator variable = "spam"
...into this:
variable = decorator.__decoration_call__(variable, "variable")
With the behavior of this variable decoration defined as shown above, here are some of the interesting possibilities I was considering.
One possibility is avoiding the pitfalls of name repetition in the RAD framework context previously mentioned by Stéfane Fermigier in Jeremiah's variable decorator thread (see his post for examples). It could see other frameworks, like web frameworks, making use if this too.
(Here I will repeat some of the possibilities suggested by Jeremiah in his first post) The standard library and other library functions could, as needed, make use of
__decoration_call__
as they see fit. I could see factories like namedtuple, make_dataclass, several typing factories, and Enum, implementing
__decoration_call__ so you can say things like:
@namedtuple Point =
"x y z"
@make_dataclass Point =
[("x", int), ("y", int), ("z", int)]
@typing.TypeVar T = (str, bytes)
@typing.NewType UserId = int
@enum.Enum Colors =
"RED GREEN BLUE"
Admittedly, these examples are not all that compelling on their own, but I think they read really well and they would allow you to avoid retyping a name (and potentially making a mistake in retyping it), so I think I really like it.
___
SIDEBAR
I also note that the above examples use a different idiom than suggested by Jeremiah. He has suggested these:
@namedtuple("x y z") Point
@make_dataclass([("x", int), ("y", int), ("z", int)]) Point
@typing.TypeVar(
(str, bytes)
) T
@typing.NewType(int) UserId
@enum.Enum("RED GREEN BLUE") Colors
...but using my proposal I do not see a way to make this idiom easily work with the existing factories. I am not sure which of these two idioms people would prefer. I can see pros and cons for both idioms. If people preferred the second idiom, new decorator objects could be added for it.
___
One interesting possibility might be to modify inspect.getsource so that it can retrieve the RHS of the line on which it appears*:
@inspect.getsource my_var = 1/2
print(my_var)
# Result
# '@inspect.getsource my_var = 1/2'
* NOTE: I am admittedly hand-waving this. Maybe there are reasons getsource would not be able to do this?
Using the rhs string from this new version of getsource, it would be a pretty simple matter for a symbolic math library like sympy to replace the float value resulting from 1/2 in my_var with a symbolic math value:
@sympy.sympify my_var = 1/2
# Result is not 0.5, but:
# 1/2
...or create a Fraction (using a modified Fraction type):
@Fraction my_var = 1/2
# Result is not 0.5, but:
# Fraction(1, 2)
..or a Decimal
(using a modified Decimal type):
@Decimal my_var = 0.5
# Result is not 0.5, but:
# Decimal('0.5')
MAYBE A DEFAULT DUNDER AFTER ALL...?
One final possibility: I have said above we would not supply any default __decoration_call__ method, and instead fallback on __call__. But it might be beneficial to supply a default __decoration_call__ with the following implementation:
def
__decoration_call__(self, func, by_name):
if func is None:
return self(by_name)
return self(func)
This would allow things like this, "out of the box":
@typing.NewType Color = str
@Color
GREEN
...which becomes:
Color = typing.NewType.__decoration_call__(str, "Color")
GREEN =
Color.__decoration_call__(None, "GREEN")
In the above, Color is just a stand-in for str. If there is a default
__decoration_call__ method as I wrote above, then:
Color.__decoration_call__(None, "GREEN")
...is actually just:
str.__decoration_call__(None, "GREEN")
..and the default implementation just returns:
str("GREEN")
...and assigns it to the name, GREEN.
Guess I'll leave it at that. Let's see how this is received.
---
Ricky.
"I've never met a Kentucky man who wasn't either thinking about going home or actually going home." - Happy Chandler