-
-
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
trio.run() should take **kwargs in addition to *args #470
Comments
Hmm, this has come up a few times, and it's a totally reasonable question – I think we need some sort of FAQ entry for this... maybe in the tutorial? Will think about it. Anyway, here's the problem: a function like BTW, in ordinary usage you wouldn't use What we do is recommend people do Rejected alternatives include:
If you've got a better idea definitely do let me know but... what we do now really is the best option available, AFAICT :-). |
Here's another option I don't see listed. What about not supporting passing configuration options directly to There's something to be said for a clean function signature that doesn't mix arguments for |
Interesting idea! And for
And then trio-asyncio has the same problem for its functions to call trio-from-asyncio and vice-versa, and I'm sure this will keep cropping up around the ecosystem. Adding a configobject interface for all of these seems really cumbersome? And I'd rather have just one solution to this problem that gets used everywhere, so you only have to learn it once, rather than two different ones so then you have to learn both solutions, and which solution is used where. |
Okay, good to know. There's yet another hybrid pattern / approach you could use for all of these. Namely, rather than calling, say, More generally, using this pattern means that you would have at most one reserved keyword argument name per function. The argument name could potentially be the same across all of the functions, though the class could vary. When you want to add more options to some function, you could add to the appropriate class without needing to touch any of the function signatures. Maybe you could even do something clever with an optional positional argument, so you wouldn't need to choose and reserve a name. |
Ah, yeah, that's similar to the
That's true, you could do something like: def run(*args, **kwargs):
# Real code would need more fiddly error-checking but this should give the idea
if isinstance(args[0], Options):
options = args.pop(0)
fn = args.pop(0)
... ...and then it'd be used like: run(fn, foo, bar)
# or
run(Options(clock=whatever), fn, foo, bar) This feels very surprising though in the Python context. Again, these can work but... the only real downside to (FWIW, I stole the Maybe the tutorial should just get a short discussion of |
Thanks for summarizing. You stated it well. Yeah, I'm familiar with the
It might be worth pointing out that you can also use funcall syntax for dicts, e.g. Incidentally, if you'll indulge me, I can't help mentioning a vague, not-fully-formed idea I've had for some time, which is for Python to expose an object that could be used to encapsulate |
I guess my convention is to always use either
Yeah, it really comes down to taste. On balance I think the advantages of being able to write
¯\_(ツ)_/¯
Huh, no, I haven't seen that idea before. |
Personally I'd like to see python add syntax-level support for partial application, then the equivalent of |
@buhman if you can convince python-dev of that then we can certainly revisit this :-). |
Duplicate of #88, similar to python-attrs/attrs#22. (FWIW I'm very strongly in favor of |
Aliasing from functools import partial as P
trio.run(P(func, a, b, c=42)) |
I have suggested, half in jest, that trio should add It might actually be worth considering seriously. |
There was a substantial discussion/brainstorming session about this in chat yesterday, starting around here: https://gitter.im/python-trio/general?at=5c19957cb8760c21bbed7ee9 I think there are 3 reasons there's ongoing tension here: (1) intuitively it seems like people want to pass through kwargs to their function a lot more than they want to use the relatively obscure configuration arguments like Some ideas from the thread:
Occasionally there are rumblings about adding some kind of macro/quoting syntax to Python. If that happens I guess we could have None of these strike me as obviously superior to what we're doing now, but there are some new ideas I wanted to capture. |
I kinda like |
More brainstorming in chat: https://gitter.im/python-trio/general?at=5c4a4449f780a1521f638f59 |
Put my 2¢ in @ https://gist.github.com/njsmith/ad4fc82578239646ccdf986ae3ca07c1 Curious if anyone else thinks that's a good api? |
Another idea, see https://gitter.im/python-trio/general?at=5c4b3e3e20b78635b67fa78e Basically this would allow you to call |
It's certainly possible to switch behaviour on whether or not The verbosity of options/config/with_options does grate a little but it has the benefit of being pretty explicit and obvious. |
Another api I was considering was forcing users to use a method to call the func, thereby leaving the class takes_callable(object):
def __init__(self, wrapped_fn, **options):
self.wrapped_fn = wrapped_fn
self.options = options
def call(self, fn, *args, **kwargs):
return self.wrapped_fn(
partial(fn, *args, **kwargs),
**self.options
)
def __call__(self, **options):
return takes_callable(self.wrapped_fn, **options) In [41]: @takes_callable
...: def run(fn, **options):
...: print(f"options = {options!r}")
...: return fn()
In [42]: def func(*args, **kwargs):
...: print(args)
...: print(kwargs)
In [43]: run.call(func, 1, 2, q=3)
options = {}
(1, 2)
{'q': 3}
In [44]: run(clock=None).call(func, 1, 2, q=3)
options = {'clock': None}
(1, 2)
{'q': 3} I think this might be a cleaner api but also less obvious/discoverable |
IMO the Splitting the spec of the function call over multiple parameters of the As trio does not necessarily need the
E.g. |
Doing some paid work tonight, I'm being annoyed again by having to use I think at this point we can be fairly confident we've exhausted all the different combinations of I'm thinking about revisiting one of the ideas we rejected early on: of using underscores to mark configuration-kwargs, versus no-underscore to mark passthrough-kwargs. So example usage would be: await trio.run(main, _clock=MyClock)
nursery.start_soon(async_fn, arg1, arg2, kwarg=foo, otherkwarg=bar, _name="something")
await trio.run_sync_in_worker_thread(container.logs, stdout=True, stderr=False, _limiter=my_limiter)
await serve_tcp(handler, _port=80)
await serve_ssl_over_tcp(handler, _port=443, _ssl_context=my_context) One downside is that it's weird-looking. But I guess we'd get used to it? I think The more technical potential downside is lack of compositionality/universality: what if we want to pass through a kwarg that starts with an underscore? For example,
The implementation would be extremely simple. E.g.: async def run(async_fn, *args, **kwargs, _clock=None, _instruments=()):
check_no_underscores(kwargs) # raises an error if any keys start with _
... No magic decorators, so it's totally understandable by naive readers, sphinx, mypy, and pylint. The one wrinkle is that mypy/pylint wouldn't know to flag |
Oh, wait, there's a second kind of compositionality problem. There are places where trio generates a kwarg: in particular, But, it's not a theoretical problem: the What options do we have available?
Are we worried about similar issues for the I don't feel like I've fully wrapped my head around the issues here. |
Meh. I still think that I don't like magic underscores; "internal use only" is not the same as "strip the under and feed me to the next part". I'd hate to be required to again resort to I'd leave |
IMHO having I personally like @smurfix's solution the best (maybe |
The discussion around PEP 570 made me realize an interesting corner case... even if we have a signature like: async def run(async_fn, *args, **kwargs): ...then technically it's not quite true that you can pass through any kwargs. Specifically, you cannot pass through a kwarg named This may not really matter in practice (in the unlikely case that someone hits it they can use |
Here is a perspective that I don't rigidly or fully stand by but which is important and I haven't seen said here:
Personally, both in my own code and at work,
And I have been happier ever since, enjoying in particular a greater smoothness of thought due to no longer having to do a depth-first search of the possible future effects and uses of this kind of argument passthrough. I do get frustrated having to write calls to
with less overhead. And that makes the upfront nuisance of writing TL;DR: forcing people to always use |
@mentalisttraceur That was me, at #470 (comment) I dunno about nursery.start_soon(
trio.to_thread.run_sync.options(cancellable=True),
func, *args, **kwargs
) having to attach |
No -- but I do think of the "dunder namespace" as fundamentally belonging to the language implementation, not to library authors. Some libraries do use it, true, but generally as something that "looks like" how the implementation uses it: a vaguely "magical" method or attribute name. Seeing dunder keyword arguments feels especially weird to me -- if I saw that in some unfamiliar code, I'd wonder whether I should consult the target method's documentation or Python's. I'm sure I'll get used to it if it's the way things are, but I suspect I'm not the only one who will find it somewhat off-putting at first.
I like this approach, and I do think we can type it using PEP 612. The implementation will need a bit of descriptor trickery to remember which nursery it should eventually call |
With the custom_start_soon = nursery.start_soon.options(name="foo")
custom_start_soon(f, spam='eggs') (Maybe the docs could then even briefly note that this can be combined into a 1-liner like In the case of |
@njsmith Yeah, that is much simpler. Nice. The only downside I can think of with your example implementation is that I hear you on If the |
|
@smurfix Yeah that's a good start. However:
But my point was just that it takes extra lines for each additional bit of functionality - if we want the result of All doable, but when it's all done every variant I've thought of is a lot more lines than just the very nice and simple variant up above. But of course it becomes simpler if we just say "we don't care about it being pickleable", "we don't care about |
Prettying up the No, I have to admit that I don't care at all for callables to be pickle-able. Doing that is rarely if ever good design. Besides, except for |
Okay, if pickling is discarded, then I think a traditional function-style decorator is sufficient to cover all docstrings and def takes_callable(wrapped):
@functools.wraps(wrapped)
def wrapper(fn, /, *args, **kwargs):
return wrapped(partial(fn, *args, **kwargs))
name = f'{wrapped.__module__}.{wrapped.__qualname__}'
def bind_options(self, **options):
f"""Bind options to {name}"""
def call_with_options(fn, /, *args, **kwargs):
f"""Call {name} with options: {options}"""
return wrapped(partial(fn, *args, **kwargs), **options)
# Enable `).call(` as an alternative to `)(`:
call_with_options.call = call_with_options
return call_with_options
wrapper.options = bind_options
return wrapper Which still feels a little too complex, but it's the best I can do so far while
|
https://docs.python.org/3/reference/lexical_analysis.html#reserved-classes-of-identifiers
(pretty sure this doesn't even qualify as two cents but...) |
@altendky I actually think this is very important to bring up! Up until now I thought Python just took a casual "we use dunder identifiers for internal stuff sometimes" position, but it sounds like Python is officially taking the Serious(tm) "we reserve all dunder identifiers for internal usage at any time" position. |
A new PEP could be the best way forward in case anyone wants to comment: I guess |
Well, we could simply alias +1 from me. |
PEP 637 was rejected, so that won't be an option. |
I want to point out another thing to have in mind related to usability and user experience:
Now that And as I'm making a PR here (at least as a conversation starter/re-starter): #2208 It has a proposal of adding some 3 new alternative functions that just wrap the current ones and have these typing tricks. As an example, here's how it could look like when sending tasks to a worker thread (similar for Nurseries, more examples in the PR):
|
Hi, I'm sorry if this is covered in the discussion above and I missed it, but why can't there be a simpler, underlying interface along the lines of:
This introduces trivial overhead and would give a verbose option for developers who need the flexibility. |
|
Yes, that's why you still offer |
It increases the API surface to little benefit; I don't think there would be appetite for the change. I don't understand what's causing problems for you about |
@oremanj Yes,
@noahbkim did I understand the problem correctly? I do wonder whether there's a rationale for |
I'm not really a fan of complicating Trio's API to support caching use cases that I think will be pretty uncommon, especially since it's easy to write |
There's a surprisingly good reason, described in CPython issue 3564: if |
this would greatly enhance usability — especially for function declarations like:
async def request(method, url, *, pool=None, preload_content=False):
The text was updated successfully, but these errors were encountered: