Skip to content

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

Closed
wants to merge 10 commits into from

Conversation

pradyunsg
Copy link
Member

@pradyunsg pradyunsg commented Dec 22, 2016

Fixes #1737

Essentially, this adds the ability to:

  • provide suggestions when the user mistypes
  • auto-correct the user, if they opt-in, using the best matching command name

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
Copy link
Member Author

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
Copy link
Member Author

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.

@@ -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):
Copy link
Member Author

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?

Copy link
Member Author

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.

close_commands = get_close_matches(name, commands_dict.keys())

if close_commands:
return close_commands[0]
else:
return False
return 0, None
Copy link
Member Author

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.

return heapq.nlargest(n, result)


def get_similar_command(name):
Copy link
Member Author

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)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

logger.warning

Copy link
Member Author

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:
Copy link
Member

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.

Copy link
Member Author

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.

Copy link
Member Author

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.

Copy link
Member

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.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cool.

return heapq.nlargest(n, result)


def get_similar_command(name):
"""Command name auto-correct."""
Copy link
Member

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

@pradyunsg
Copy link
Member Author

pradyunsg commented Dec 23, 2016

This is how it currently looks:

$ pip goooo
ERROR: pip does not have a command "goooo"
$ pip lost
ERROR: pip does not have a command "lost" - did you mean "list"?
$ pip lst
You called a pip command named "lst" which does not exist.
Assuming you meant "list", pip will continue in 2.0 seconds...
^C

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
Copy link
Member

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...

Copy link
Member Author

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. 😅

Copy link
Member Author

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.

@pradyunsg
Copy link
Member Author

Pinging @dstufft, because he authored #1737.

@pradyunsg
Copy link
Member Author

@xavfernandez @dstufft Any outstanding issues with this?

@pradyunsg
Copy link
Member Author

Bump.

@pradyunsg
Copy link
Member Author

Bump 2.0?

Copy link
Member

@dstufft dstufft left a 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)
Copy link
Member

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?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed! Will change.

Copy link
Contributor

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

Copy link
Member

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.

Copy link
Member

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

Copy link
Member

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.

Copy link
Member Author

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...

Copy link
Member

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)
Copy link
Member

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.

Copy link
Member Author

@pradyunsg pradyunsg Mar 16, 2017

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?

@xavfernandez xavfernandez added this to the 10.0 milestone Mar 16, 2017
@xavfernandez
Copy link
Member

It is also now missing a changelog file (cf https://pip.pypa.io/en/latest/development/#adding-a-news-entry)

@pradyunsg
Copy link
Member Author

@xavfernandez Will do!

@BrownTruck
Copy link
Contributor

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 master branch into this pull request or rebase this pull request against master then it will eligible for code review and hopefully merging!

@BrownTruck BrownTruck added the needs rebase or merge PR has conflicts with current master label Apr 1, 2017
@dstufft
Copy link
Member

dstufft commented Apr 1, 2017

Ignore this, Just testing something.

@dstufft dstufft closed this Apr 1, 2017
@dstufft dstufft reopened this Apr 1, 2017
@BrownTruck BrownTruck removed the needs rebase or merge PR has conflicts with current master label Apr 1, 2017
@pradyunsg pradyunsg force-pushed the autocorrect-command branch from 1cc216d to 77a5836 Compare April 7, 2017 17:28
@pradyunsg
Copy link
Member Author

pradyunsg commented Apr 7, 2017

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.

@pradyunsg pradyunsg mentioned this pull request May 12, 2017
9 tasks
@BrownTruck
Copy link
Contributor

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 master branch into this pull request or rebase this pull request against master then it will eligible for code review and hopefully merging!

@BrownTruck BrownTruck added the needs rebase or merge PR has conflicts with current master label May 19, 2017
@pradyunsg pradyunsg changed the title [WIP] Add the ability to autocorrect a user's command Add the ability to autocorrect a user's command Jun 14, 2017
@pradyunsg pradyunsg force-pushed the autocorrect-command branch from d81d47e to d20ec7d Compare June 14, 2017 07:42
@pradyunsg
Copy link
Member Author

/request-review @dstufft @pfmoore @xavfernandez

@pradyunsg
Copy link
Member Author

With a messy looking recent history, this PR is ready for review. ^.^

@pfmoore
Copy link
Member

pfmoore commented Jun 14, 2017

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.

@dstufft
Copy link
Member

dstufft commented Jun 17, 2017

I use it in git where I find it useful because I regularly typo things like git psh or something. I don't feel super strongly though about it, I just thought it might be useful?

@pradyunsg
Copy link
Member Author

I use it in git where I find it useful

Same. And, I have mistyped pip insatll multiple times. And it's opt-in, so, I guess this won't hurt?

@pradyunsg
Copy link
Member Author

@pfmoore @dstufft Will we be going ahead with this?

@pradyunsg
Copy link
Member Author

After a week, ping!

@pradyunsg pradyunsg dismissed dstufft’s stale review June 26, 2017 18:33

The entire thing has been re-implemented since.

@pradyunsg pradyunsg added the type: enhancement Improvements to functionality label Jun 26, 2017
@pfmoore
Copy link
Member

pfmoore commented Jun 26, 2017

I won't be merging it myself. But I'm not going to argue if @dstufft does.

@pradyunsg
Copy link
Member Author

Ping @dstufft!

@pradyunsg pradyunsg self-assigned this Jun 29, 2017
@dstufft
Copy link
Member

dstufft commented Jul 5, 2017

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 git, there's pretty much not much you can do that can't be undone, so auto correcting commands is fairly safe. However with pip there's no easy way to undo a command, so guessing wrong has a much higher cost than it does in git.

So primarily for that reason, I'm going to reject this PR. Thanks @pradyunsg for taking a look at this though!

@dstufft dstufft closed this Jul 5, 2017
@pradyunsg pradyunsg deleted the autocorrect-command branch July 6, 2017 07:27
@pradyunsg
Copy link
Member Author

I'm going to reject this PR.

No issues. ^.^

Thanks @pradyunsg for taking a look at this though!

You're welcome!!

@pradyunsg pradyunsg removed their assignment Oct 5, 2017
@lock
Copy link

lock bot commented Jun 2, 2019

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.

@lock lock bot added the auto-locked Outdated issues that have been locked by automation label Jun 2, 2019
@lock lock bot locked as resolved and limited conversation to collaborators Jun 2, 2019
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
auto-locked Outdated issues that have been locked by automation type: enhancement Improvements to functionality
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Don't tell the user what they meant, just do what they meant
8 participants