New Feature: Add the ability to pipe args and kwargs.
tallerasaf opened this issue · 6 comments
PR 'Add the ability to pipe args and kwargs.' -> #23
I added the functionality to pipe *args and **kwargs to a function.
And now you don't need to pipe a tuple with the first argument as a function and the second argument as a parameter
Now you can pass *args and **kwargs to a function using pipe '|'.
Now prepare_function_for_pipe knows how to handle keyword-only arguments.
And or knows how to handle next_func as *args and **kwargs to self.func.
# Automatic partial with *args
range_args: tuple[int, int, int] = (1, 20, 2)
# Using pipe
my_range: Callable = pipe | range | range_args
# Using tuple
my_range: Callable = pipe | (range, range_args)
# list(my_range()) == [1, 3, 5, 7, 9, 11, 13, 15, 17, 19]
# Automatic partial with **kwargs
dataclass_kwargs: dict[str, bool] = {'frozen': True, 'kw_only': True, 'slots': True}
# Using pipe
my_dataclass: Callable = pipe | dataclass | dataclass_kwargs
# Using tuple
my_dataclass: Callable = pipe | (dataclass, dataclass_kwargs)
@my_dataclass
class Bla:
foo: int
bar: str
# Bla(5, 'bbb') -> Raises TypeError: takes 1 positional argument but 3 were given
# Bla(foo=5, bar='bbb').foo == 5
Hey @tallerasaf! Can you please describe the new functionality and what use cases does it solve?
Hey @tallerasaf! Can you please describe the new functionality and what use cases does it solve?
I added the functionality to pipe *args and **kwargs to a function.
And now you don't need to pipe a tuple with the first argument as a function and the second argument as a parameter
Now you can pass *args and **kwargs to a function using pipe '|'.
Now prepare_function_for_pipe knows how to handle keyword-only arguments.
And or knows how to handle next_func as *args and **kwargs to self.func.
# Automatic partial with *args
range_args: tuple[int, int, int] = (1, 20, 2)
# Using pipe
my_range: Callable = pipe | range | range_args
# Using tuple
my_range: Callable = pipe | (range, range_args)
# list(my_range()) == [1, 3, 5, 7, 9, 11, 13, 15, 17, 19]
# Automatic partial with **kwargs
dataclass_kwargs: dict[str, bool] = {'frozen': True, 'kw_only': True, 'slots': True}
# Using pipe
my_dataclass: Callable = pipe | dataclass | dataclass_kwargs
# Using tuple
my_dataclass: Callable = pipe | (dataclass, dataclass_kwargs)
@my_dataclass
class Bla:
foo: int
bar: str
# Bla(5, 'bbb') -> Raises TypeError: takes 1 positional argument but 3 were given
# Bla(foo=5, bar='bbb').foo == 5
Sorry this just seems really confusing to me. You want the ability to pipe a callable into its own arguments? What problem does that solve?
It's confusing because if you just see a | b | c
you wouldn't know what is a callable and what is arguments. Whereas with a tuple it's clearly separated. Also it seems backwards in terms of piping which should be just things flowing through a pipe.
So suddenly you'd have 2 ways to do partial application and |
would mean 2 different things, which goes against simplicity and I don't really see any benefits.
What could be useful is ability to somehow nicely do partial application of kwargs. However how would you distinguish between applying keyword arguments and applying a single positional argument of a dictionary?
def func(a, b=1, c=None):
...
pipe | (func, {'a': "hello", 'b': 3, 'c': X}) # what happens now?
For now probably best we can do is:
from pipetools import pipe, xpartial as P
pipe | P(func, a="hello", b=3, c=X)
Sorry this just seems really confusing to me. You want the ability to pipe a callable into its own arguments? What problem does that solve?
-> I want the ability to pipe *args and **kwargs to a function as a partial function, it solved the problem of adding a new function that is a combination of several functions and functions with some of their arguments as a partial function in one line.
It's really useful for decorators.. many decorators accepts arguments(*args or **kwargs) so now you can chain multiple decorators to one decorator.
So I will be able to do:
from dataclasses import dataclass
from typing import Callable
from dataclasses_json import dataclass_json
from pipetools import pipe
dataclass_kwargs: dict[str, bool] = {'frozen': True, 'kw_only': True, 'slots': True}
my_dataclass: Callable = pipe | dataclass | dataclass_kwargs | dataclass_json
@my_dataclass
class Bla:
foo: int
bar: str
Instead of:
from dataclasses import dataclass
from dataclasses_json import dataclass_json
@dataclass_json
@dataclass(frozen=True, kw_only=True, slots=True)
class Bla:
foo: int
bar: str
It's confusing because if you just see
a | b | c
you wouldn't know what is a callable and what is arguments.
-> You can distinguish it by the variable name and by the variable type(with type hinting).
So suddenly you'd have 2 ways to do partial application and
|
would mean 2 different things, which goes against simplicity and I don't really see any benefits.
This is much more simpler:
my_dataclass: Callable = pipe | dataclass | dataclass_kwargs | dataclass_json
Then this one:
my_dataclass: Callable = pipe | (dataclass, dataclass_kwargs) | dataclass_json
What could be useful is ability to somehow nicely do partial application of kwargs. However how would you distinguish between applying keyword arguments and applying a single positional argument of a dictionary?
-> It will be a convention' because even if you want one variable to be a dictionary you can use the keyword argument, for example:
def func(my_dict: dict, number: int, text: str):
pass
my_pipe = pipe | (func, {'my_dict': {'a': "hello", 'b': 3, 'c': X}, 'number': 5, 'text': 'bla'})
# Or
my_pipe = pipe | func | {'my_dict': {'a': "hello", 'b': 3, 'c': X}, 'number': 5, 'text': 'bla'}
If you want only a single keyword argument of a dictionary you can do:
def func(my_dict: dict):
pass
my_pipe = pipe | (func, {'my_dict': {'a': "hello", 'b': 3, 'c': X}})
# Or
my_pipe = pipe | func | {'my_dict': {'a': "hello", 'b': 3, 'c': X}}
Or If you want only a single positional argument of a dictionary you can do:
def func(my_dict: dict):
pass
my_pipe = pipe | (func, ({'a': "hello", 'b': 3, 'c': X}))
# Or
my_pipe = pipe | func | ({'a': "hello", 'b': 3, 'c': X})
So to sum up:
For keyword arguments you will pass a dictionary exactly like **kwargs:
def func(my_dict: dict, my_tuple: tuple, number: int, text: str):
pass
my_pipe = pipe | func | {'my_dict': {'a': "hello", 'b': 3, 'c': X}, 'my_tuple': (1, ,2, 3), 'number': 5, 'text': 'bla'}
For positional arguments you will pass a tuple exactly like *args:
def func(my_dict: dict, my_tuple: tuple, number: int, text: str):
pass
my_pipe = pipe | func | ({'a': "hello", 'b': 3, 'c': X}, (1, 2, 3) 5, 'bla')
@0101 What do you think?
@tallerasaf I'm just not seeing it. In both your examples I think the current case is the simpler one. And the kwargs looks like it would be complicated to get right, but mainly it would not be backwards compatible. And I don't want anyone to update and get weird errors. Sorry.