-
-
Notifications
You must be signed in to change notification settings - Fork 1.7k
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
Add Command.awaitHook(), Argument.chainArgParserCalls() and Option.chainArgParserCalls() #1902
Conversation
Interesting that this implicitly supports defaults which are promises. I am not certain this is robust enough. Some code has run and tested the promises rather than the values, like the processing of implied values. But the code I looked through only cared about the value being defined, and Commander can't make assumptions about what a custom parser returns, so may actually be ok. |
lib/command.js
Outdated
|
||
awaitHook() { | ||
this.hook('preAction', async function awaitHook(thisCommand, actionCommand) { | ||
actionCommand.processedArgs = await Promise.all( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
processedArgs
may include an element which is an array for a variadic argument. I am guessing this is not supported by Promise.all
so might need more complex processing.
lib/command.js
Outdated
|
||
for (const [key, value] of Object.entries(actionCommand.opts())) { | ||
actionCommand.setOptionValueWithSource( | ||
key, await value, actionCommand._optionValueSources[key] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
-
The option value may be an array of values for a variadic option.
-
I think each call to await will delay execution until the next tick. I suggest only call for
thenable
values. -
I would slightly prefer not to call
setOptionValueWithSource
on non-promises, which also fits in with testing forthenable
in above item.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There is an existing test for thenable
here:
Line 1192 in 4ef19fa
if (promise && promise.then && typeof promise.then === 'function') { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
- Due to the "previous value" paradigm of custom variadic option processing, the only possible sources for such an array are default and implied values which are assumed to be preprocessed and should ideally be left untouched. If the array really needs to be awaited elementwise, one could simply use
Promise.all(arr)
as the default / implied value. The same applies to arguments. - Solved by 4738e3c.
- Solved by 4738e3c.
I am intrigued whether this will be good enough to build in rather than be opt-in! See how it goes... |
The few lines will cover many programs, but it is harder building the code into Commander where we have to consider all the ways arguments and options may be declared and used. |
A paranoid thought. Are repeated (non-variadic) options a problem because they will create dangling promises? Suppose lookup does an async parseArg. The promise for
|
4a6e07c introduces a breaking change which is necessary to enable asynchronous custom processing of variadic arguments and options. |
Dangling pointers don't really pose any threat as long as their exceptions are caught, which I took care for in 4738e3c by running |
On second thought, there might be use cases where custom processing is expected to produce promises without chaining them, for example program.argument('<file...>', 'files', (filename, promise) => {
const newPromise = new Promise((resolve, reject) => {
// Do something with filename...
});
return promise ? Promise.all([promise, newPromise]) : newPromise;
}); So maybe this change does not have to be breaking and should be made available via a method, e.g. |
Or via an argument method for that matter: |
Good point that promises and variadic only happen with a custom parseArgs, and author is in control of what is stored. (This was a design decision when adding variadic as parseArgs can be more than just a coercion.) |
- Do not await default argument values since custom processing does not apply - Leave thenable checks to `Promise.all()`
Line 1181 turned out to be not enough since After spending a lot of time figuring out these unhandled rejections, I now understand your concern about repeated non-variadic options better. The code passes the test I wrote for this case without errors, but I honestly have no idea why the parser only gets called once. |
Oh, now I see it is because we treat non-variadic options the same way as variadic when they are repeated and a parser is provided, so as long as we chain calls to |
Problem solved by creating an object with backups of overwritten values. Should it just be restricted to thenable values? |
This comment was marked as resolved.
This comment was marked as resolved.
Ok, now asynchronous custom processing ( |
What use case is I was wondering about dropped promises, but perhaps that is relatively easy for the author to handle. One of the custom parse examples is:
If we make that async, could it just be:
If we were were doing a simple one-value coerce/parse, but similarly wanting to avoid dangling promises:
|
I guess it is just a shortcut that makes it possible to write async parsers that work with two values just like normal ones, rather than with one value and one promise. Quite an artificial example, but imagine there is a deeply nested structure somewhere on a server, and we are writing a program that accepts keys needed to access a certain property nested in the structure as arguments. We want to join the keys into one key string the server understands, but also we want all substructurs accessed on the way to the property to satisfy certain criteria for the input to be considered valid, and the only way to check it is by sending requests to the server for all partial key strings constructed along the way. The parser could then look like this: async function parseKeyPart(value, previous) {
const key = previous ? `${previous}.${value}` : value;
if (!(await satisfiesCriteria(key))) {
throw new InvalidArgumentError(
`Partial key ${key} does not satisfy the criteria`
);
}
return key;
} Update: Too much work for an argument parser, I agree, but it does not have to be a server, could just as well be a local database only ever accessed once but asynchronously, so the condition would have to be something like !(satisfiesCriteriaInDb(await dbPromise, key)) That would imply no more work for the parser to do than in my original example in #1900. So there are actually some cases where |
Some musing, I am not sure of how best to approach these, just at the thinking stage. I don't think the existing hooks are a good match for settling the option promises at the higher levels of a deeply nested command, which I think are not currently covered. It might make sense to do that in That leaves the options on the leaf command itself and the processed arguments, which are currently covered by the A complication is that the various places that |
For a user implementation of this, walking up the command chain from |
Well, then |
Could you please elaborate on this? I am not sure I understand the problem. Is it about the risk of unhandled promise rejections again? |
You probably mean the fact the exception is not caught in the following code, although it is when an action is added: program
.argument('arg', 'desc', async() => {
throw 123;
})
.awaitHook();
try {
await program.parseAsync(['abc'], { from: 'user' });
} catch {
console.log('caught');
} Well, in that case, adding a new But also, it is kind of weird that the following snippets produce different results. program
.argument('arg', 'desc', () => {
throw 123;
});
try {
program.parse(['abc'], { from: 'user' });
} catch {
console.log('caught');
} program
.argument('arg', 'desc', async() => {
throw 123;
});
try {
await program.parseAsync(['abc'], { from: 'user' });
} catch {
console.log('caught');
} I am thinking whether it would be reasonable to make the behavior of |
Not sure I will have time for this in the coming weeks, so I just went on and implemented the changes I meant.
Callbacks are added for both The code is much more complex now and will require thorough testing. |
Could fix some old tests in tests/command.hook.test.js if preAction is replaced by postArguments.
Yes. This is getting too complex for my liking. High level, your idea of using the stored promises from calling I wonder how simple this could be leaving out extras like Adding a few lines to the "Parsing life cycle and hooks" description, there looks like good places to place the extra calls. Starting with top-level command (program):
Once reach final (leaf) command:
|
Either way, the same behavior should be implemented for
So as you can see, some of the new complexity is unavoidable if we want to have the hook enabled by default:
This is what the "Parsing life cycle and hooks" description should look like if we do not use the existing hook infrastructure but keep the Parsing life cycle and hooks
|
Yes, that was what I had in mind. I opened a PR with an experiment in #1913 |
Co-authored-by: John Gee <[email protected]>
I have implemented the simplifications in #1915. |
Using the existing hook infrastructure turned out to be a bad idea giving nothing but unnecessary complexity. Closing in favor of #1915. |
Pull Request
Problem
#1900
At the moment, a few lines of extra code are required to enable intuitive use of asynchronous custom processing of arguments and option-arguments by means of
async
argument parsers.Solution
The extra code is wrapped in the
awaitHook()
method of theCommand
class. It additionally awaits values that the custom processing is not applied to, namely default argument values as well as default and implied option values.A
chainArgParserCalls()
method causing thenables returned fromparseArg()
to be chained is added to bothArgument
andOption
classes. This is relevant whenparseArg()
is called with second argument supplied, which is the case for variadic arguments and options and repeated non-variadic options.