-
Notifications
You must be signed in to change notification settings - Fork 3.1k
Add the ability to autocorrect a user's command #4193
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
Conversation
pip/__init__.py
Outdated
# all the args without the subcommand | ||
cmd_args = args[:] | ||
cmd_args.remove(cmd_name) | ||
|
||
# Autocorrect command name | ||
if cmd_name not in commands_dict: | ||
# MARK: The following should be loaded from the configuration file |
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.
This part probably depends on #4192. I'm willing to shoot up another PR (or update this one) once that merges.
pip/__init__.py
Outdated
score, guess = get_similar_command(cmd_name) | ||
|
||
if guess is None: # nothing similar | ||
err_msg = 'pip does not have a command "%s"' % cmd_name |
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.
I changed the error message. It's nicer IMO.
pip/commands/__init__.py
Outdated
@@ -61,18 +61,42 @@ def get_summaries(ordered=True): | |||
yield (name, command_class.summary) | |||
|
|||
|
|||
def get_similar_commands(name): | |||
def get_close_matches(word, possibilities, n=1, cutoff=0.6): |
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.
This is a re-implementation that's basically copy-paste removing the last line. Why it's duplication, I don't imagine this code changing anyway. Thoughts?
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.
Implemented a better function for this.
pip/commands/__init__.py
Outdated
close_commands = get_close_matches(name, commands_dict.keys()) | ||
|
||
if close_commands: | ||
return close_commands[0] | ||
else: | ||
return False | ||
return 0, None |
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.
This change made life easier when handling results at call-sites.
pip/commands/__init__.py
Outdated
return heapq.nlargest(n, result) | ||
|
||
|
||
def get_similar_command(name): |
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.
Name change because only one command is returned.
pip/__init__.py
Outdated
'Assuming you meant "%s", pip will continue in %.1f seconds...' | ||
) | ||
|
||
logger.warn(msg, cmd_name, guess, wait_time) |
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.
logger.warning
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.
Okay.
pip/__init__.py
Outdated
# it returns | ||
for i in range(int(wait_time * 10)): | ||
time.sleep(0.1) | ||
except KeyboardInterrupt: |
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.
Let the exception bubble up until BaseCommand
.
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.
I don't see BaseCommand anywhere in the call stack.
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.
BaseCommand doesn't come into the picture since this is directly called by main
to compute which command to initialize. Letting this exception bubble up results in a not-so-nice traceback, since there is no one catching KeyboardInterrupt
in a "upper" stack position.
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.
Hmm, you are right. Then, it would be nice to print the same error messages: https://github.com/pypa/pip/blob/bbe99ce/pip/basecommand.py#L236-L237 and sys.exit(pip.status_codes.ERROR)
, to be consistent.
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.
Cool.
pip/commands/__init__.py
Outdated
return heapq.nlargest(n, result) | ||
|
||
|
||
def get_similar_command(name): | ||
"""Command name auto-correct.""" |
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.
Maybe update the docstring with the return types
This is how it currently looks:
|
pip/__init__.py
Outdated
logger.warning(msg, cmd_name, guess, wait_time) | ||
try: | ||
# time.sleep in a loop because KeyboardInterrupt is raised after | ||
# it returns |
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.
This seems strange...
$ python -c "import time;time.sleep(60)"
^CTraceback (most recent call last):
File "<string>", line 1, in <module>
KeyboardInterrupt
works right away...
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.
I sourced it from http://stackoverflow.com/questions/5114292/break-interrupt-a-time-sleep-in-python
The asker ran this on Python 2.2. I didn't notice that. 😅
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.
I'll push the correction.
f7ee09a
to
ffca676
Compare
@xavfernandez @dstufft Any outstanding issues with this? |
Bump. |
Bump 2.0? |
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.
Two comments, otherwise looks good.
pip/__init__.py
Outdated
time.sleep(wait_time) | ||
except KeyboardInterrupt: | ||
logger.critical('Operation cancelled by user') | ||
logger.debug('Exception information:', exc_info=True) |
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.
We probably don't need to record an exception for a KeyboardInterrupt
, I don't think there is any value in the traceback for debugging or anything is there?
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.
Agreed! Will change.
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.
is it really a good idea to just go with the autocorrected command?
this may hide severely broken scripts and problems for longer than necessary,
imho the tool should still fail
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.
I agree. I'm not keen on letting people type the incorrect thing and just carrying on with what we assume they meant. Are there other tools that do this? From what I know, tools that autocorrect tend to either say "did you mean X?" and stop, or say "did you mean X (Y/N)?" and prompt but not proceed without an explicit response from the user.
Having said that, I don't know many tools that do this at all, so I have limited information to work from.
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.
FWIW, I also agree that it's not good to assume what the user meant.
git
is one tool that has this behaviour:
$ git coone
git: 'coone' is not a git command. See 'git --help'.
Did you mean this?
clone
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.
Ironically, my git
automatically runs the correct command :)
$ git pll
WARNING: You called a Git command named 'pll', which does not exist.
Continuing under the assumption that you meant 'pull'
in 0.1 seconds automatically...
Already up-to-date.
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.
Git has a configuration option that can be modified by the user to enable this behavior. help.autocorrect
Since it's been brought up, we could do something like git and hide this behavior behind a configuration option...
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.
If the default is to not assume, then I'm OK with this.
pip/__init__.py
Outdated
) | ||
return None | ||
|
||
wait_time = get_float("wait_time", 2.0) |
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.
It looks like you're allowing configuration of these values here, but it doesn't appear like you've added the configuration values to the config files, so while it's looking for them, I don't think they'll ever exist.
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.
That is why I have included the fallback values (2.0 for wait_time here).
Is there a better way to have fallbacks for these or am I missing something?
It is also now missing a changelog file (cf https://pip.pypa.io/en/latest/development/#adding-a-news-entry) |
@xavfernandez Will do! |
Hello! I am an automated bot and I have noticed that this pull request is not currently able to be merged. If you are able to either merge the |
Ignore this, Just testing something. |
1cc216d
to
77a5836
Compare
I'll come back to this after #4240 - it changes a lot of things regarding the configuration objects. Those changes make using the configuration easier for this. |
Hello! I am an automated bot and I have noticed that this pull request is not currently able to be merged. If you are able to either merge the |
d81d47e
to
d20ec7d
Compare
Because consistency.
/request-review @dstufft @pfmoore @xavfernandez |
With a messy looking recent history, this PR is ready for review. ^.^ |
Honestly, I don't like this "do what I mean" type of interface. I'd be OK with a simple message that said "no command foo, did you mean fooo?" and stopped, but the timed wait and automatic running of what we guessed just feels like too much (and I'm speaking as a Windows user, who's supposed to be used to UIs doing what they think I meant 😄) I get that this is opt-in, so if others support this change then fine, but personally I'm not a fan. |
I use it in |
Same. And, I have mistyped |
After a week, ping! |
The entire thing has been re-implemented since.
I won't be merging it myself. But I'm not going to argue if @dstufft does. |
Ping @dstufft! |
I've been thinking about this and talking it over with some friends, and I think ultimately it makes sense to reject this and just close the original issue. The thing that ended up finally tipping the scales into the no camp is that with So primarily for that reason, I'm going to reject this PR. Thanks @pradyunsg for taking a look at this though! |
No issues. ^.^
You're welcome!! |
This thread has been automatically locked since there has not been any recent activity after it was closed. Please open a new issue for related bugs. |
Fixes #1737
Essentially, this adds the ability to: