In the current python codebases, there are a lot of decorators with parameters, and their code in most cases contains a lot of code boilerplates. The purpose of this issue is to add `decorator_with_params` function to `functools` module. This function will allow using a wrapped function as a simple decorator and decorator with parameter.
I believe the real example will bring more context, for instance, `typing._tp_cache` function can be rewritten from:
```
def _tp_cache(func=None, /, *, typed=False):
"""Internal wrapper caching __getitem__ of generic types with a fallback to
original function for non-hashable arguments.
"""
def decorator(func):
cached = functools.lru_cache(typed=typed)(func)
_cleanups.append(cached.cache_clear)
@functools.wraps(func)
def inner(*args, **kwds):
try:
return cached(*args, **kwds)
except TypeError:
pass
return func(*args, **kwds)
return inner
if func is not None:
return decorator(func)
return decorator
```
To
```
@decorator_with_params
def _tp_cache(func=None, /, *, typed=False):
"""Internal wrapper caching __getitem__ of generic types with a fallback to
original function for non-hashable arguments.
"""
cached = functools.lru_cache(typed=typed)(func)
_cleanups.append(cached.cache_clear)
@functools.wraps(func)
def inner(*args, **kwds):
try:
return cached(*args, **kwds)
except TypeError:
pass # All real errors (not unhashable args) are raised below.
return func(*args, **kwds)
return inner
```
And `_tp_cache` can be used as:
```
@_tp_cache(typed=True)
def __getitem__(self, parameters):
return self._getitem(self, parameters)
...
@_tp_cache
def __getitem__(self, parameters):
return self._getitem(self, parameters)
```
Proposed implementation is quite simple (I used it in a lot of projects):
```
def decorator_with_params(deco):
@wraps(deco)
def decorator(func=None, /, *args, **kwargs):
if func is not None:
return deco(func, *args, **kwargs)
def wrapper(f):
return deco(f, *args, **kwargs)
return wrapper
return decorator
```
I believe it will be a great enhancement for the Python standard library and will save such important indentations.
|
Let's wait for Nick.
I have found great examples to show how decorator_factory can simplify things.
dataclasses.dataclass
```
def dataclass(cls=None, /, *, init=True, repr=True, eq=True, order=False,
unsafe_hash=False, frozen=False):
def wrap(cls):
return _process_class(cls, init, repr, eq, order, unsafe_hash, frozen)
# See if we're being called as @dataclass or @dataclass().
if cls is None:
# We're called with parens.
return wrap
# We're called as @dataclass without parens.
return wrap(cls)
```
```
@functools.decorator_factory
def dataclass(cls, /, *, init=True, repr=True, eq=True, order=False,
unsafe_hash=False, frozen=False):
return _process_class(cls, init, repr, eq, order, unsafe_hash, frozen)
```
functools.lru_cache
```
def lru_cache(maxsize=128, typed=False):
if isinstance(maxsize, int):
# Negative maxsize is treated as 0
if maxsize < 0:
maxsize = 0
elif callable(maxsize) and isinstance(typed, bool):
# The user_function was passed in directly via the maxsize argument
user_function, maxsize = maxsize, 128
wrapper = _lru_cache_wrapper(user_function, maxsize, typed, _CacheInfo)
wrapper.cache_parameters = lambda : {'maxsize': maxsize, 'typed': typed}
return update_wrapper(wrapper, user_function)
elif maxsize is not None:
raise TypeError(
'Expected first argument to be an integer, a callable, or None')
def decorating_function(user_function):
wrapper = _lru_cache_wrapper(user_function, maxsize, typed, _CacheInfo)
wrapper.cache_parameters = lambda : {'maxsize': maxsize, 'typed': typed}
return update_wrapper(wrapper, user_function)
return decorating_function
```
```
@decorator_factory
def lru_cache(user_function, /, maxsize=128, typed=False):
if isinstance(maxsize, int):
# Negative maxsize is treated as 0
if maxsize < 0:
maxsize = 0
elif maxsize is not None:
raise TypeError(
'Expected first argument to be an integer, a callable, or None')
wrapper = _lru_cache_wrapper(user_function, maxsize, typed, _CacheInfo)
wrapper.cache_parameters = lambda : {'maxsize': maxsize, 'typed': typed}
return update_wrapper(wrapper, user_function)
```
|
Sorry, I meant to include this comment as well:
The other filter I applied here was "Could someone figure out the boilerplate code themselves in less time than it would take them to find, read, and understand the documentation for the new helper function?"
I'm not sure the answer to that question is "Yes" when writing a decorator factory for the first time, but I am confident it is "Yes" when reading a decorator factory, and for anyone writing their second and subsequent factory (especially if they use the "def factory(decorated=None, /, *, keyword="only", params=True)" idiom the way dataclass and _tp_cache do).
|