-
-
Notifications
You must be signed in to change notification settings - Fork 343
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Should serve_* take positional *args and pass them on to the handler? #563
Comments
An additional benefit here is that if people switch around from However, with mandatory kwargs, you get much better error messages, something like: |
I agree that this change would be beneficial. |
By this, do you mean this?
Or do you want to add positional args straight away, with the positional confusion, and in the same release document serve_* other args to be keyword only and issue DeprecationWarnings? |
The ideal deprecation cycle is: there should be some period of time where the old thing works the same way it used to, and the new thing works the way it will work in the future, and the old thing gives a warning. So in this case, we'd want to have some time where I guess we could add positional args straight away, with the proviso that they only work if _not_given = object()
async def serve_tcp(handler, *args, *, port=_not_given, ...):
if port is _not_given:
if len(args) != 1:
raise TypeError
warn_deprecated(...)
(port,) = args
args = ()
# Now we can act like we're in the future case
... |
Thanks for the clarification! Makes perfect sense to me. I support the
change.
Any other part of the API that still doesn’t use the same pattern? I don’t
remember any.
|
Here's a mild annoyance that happens if we make # Before
await nursery.start(serve_tcp, handler, port)
# After
await nursery.start(partial(serve_tcp, handler, port=port)) This isn't a huge showstopper or anything, but I missed it before, and it is a bit annoying given how commonly these two operations are combined. And that the original reason @dstufft suggested making this change was that he wanted to avoid using # Before
await nursery.start(serve_tcp, partial(handler, *handler_args), port)
# After
await nursery.start(partial(serve_tcp, handler, *handler_args, port=port)) |
FWIW I really really like keyword-only arguments, and would be happy to see them basically anywhere in Trio. They're harder to accidentally misuse, easier to compatibly change later, and clearer when reading calling code! If it wasn't for the whole "invalid on Python 2" thing, I'd use them for practically everything - IMO two positional arguments is a sensible limit 😄 I've also argued for more use of |
Looking at this again, I noticed another piece of potential awkwardness: if we make it so await handler(stream, *args) ...then that means that await handler(*args, stream) OTOH, @dstufft's point above is still pretty compelling – that if you have a wrapper, you can write Options to consider:
|
I remain very fond of |
Couple of thoughts:
|
Oh I totally missed replying to this one:
Understatement. You probably already see what I am about to say, but for anyone who doesn't: This would break I literally would never have thought to worry about a function doing this to me if I passed a partially applied function in, until I read this. Now I won't be able to sleep at night. "Nothing is certain... Nothing can be trusted..." I mutter as I sit curled up in the corner, rocking back and forth, terror in my wide and unblinking eyes. If I ever hit a function like this in the wild I would immediately code my own implementation of This would be like if we overloaded a substraction operator so that By the way consider:
Notice the elegant separation of concerns: you just worry about calling the callable with exactly the arguments you want to pass, in the positions/keywords it makes sense to pass them in, your caller worries about how their functions actually get those arguments, and a few simple composable functions take care of doing just that one job well. TL;DR: Modifying the order of passed in |
Wait I just realized! You can just write a generic wrapper that lets you give all def with_args_to_handler(function):
def wrapper(handler, *args, **kwargs):
return function(partial(handler, *args), **kwargs)
return wrapper Then you can just do: serve_tcp = with_args_to_handler(trio.serve_tcp) And then call it as you wanted: serve_tcp(handler, foo, bar, port=80) You can change the |
Taking that last idea further: you could even provide such wrappers with Trio. You could even provide a file within Trio that people could import, which has all the Trio functions wrapped like this. I mean it needs a little more work for class methods or whatever, but you could generalize this to the point that Trio's code never needs to pass through Then instead of a bunch of case-by-case design decisions, having to try to balance the wants of people who want to pass through Even if you leave places that already follow the " And you can use the same strategy to solve the "but I wanna pass def with_kwargs_to_handler(function):
def wrapper(handler, *args, **kwargs):
return function(partial(handler, **kwargs), *args)
return wrapper
def with_args_and_kwargs_to_handler(function):
def wrapper(handler, *args, **kwargs):
return function(partial(handler, *args, **kwargs))
return wrapper But probably give them nicer shorter names, like Edit: Note that you can't do these wrappers as generically and composably in the other direction! For every |
Now a concise final reply to the main question: I slightly recommend against, because
|
Apparently @dstufft finds it annoying that you can't pass positional arguments through
serve_tcp
. He has a point! It is weird, when basically every other function like this in trio does allow you to pass-through positional arguments.There are two issues. First, say we pass
handler
and*args
toserve_tcp
. Does this ultimately call:or does it call
? @dstufft suggests that it should call
handler(stream, *args)
, because this allows you to easily write code like:which is much more awkward with the
handler(*args, stream)
form. That seems fairly convincing, though I'm still a bit nervous that people will be confused. (Even if one option is better once you think about it a lot, that doesn't mean it's obvious to a new user!)The other issue is that if we added this right now, we'd have calls like:
It's weird how the the final call gets assembled by taking the first argument, and then skipping some arguments, and then taking the rest.
Here @dstufft also points out that we could make these kwargs, so it'd be:
This is... pretty compelling actually, even on its own merit. Trio's general rule is that in
runner(callable, *args, **kwargs)
, the positional args belong tocallable
, and the kwargs belong torunner
. Movingserve_*
's configuration arguments to kwargs would make them much more consistent with the general rule. Plus writingserve_tcp(handler, 80)
has always kind of bothered me (what's that random magic constant?). Butserve_tcp(handler, port=80)
looks quite nice. I'm actually not sure why I didn't do it this way in the first place. Maybe because I'm still stuck in the past and forgot we can actually use mandatory kwargs now?If we do implement both of these changes then it'd be a kind of disruptive transition, where every
serve_tcp
call needs to convert fromserve_tcp(handler, port)
→serve_tcp(handler, port=port)
. But I do think we can do a regular deprecation warning, so it's not wildly out of line with other API adjustments we've made.So I'm... tentatively kind of thinking this might be a good idea? Not entirely sure yet. What do y'all think?
The text was updated successfully, but these errors were encountered: