Movatterモバイル変換


[0]ホーム

URL:


Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

gh-69605: Add module autocomplete to PyREPL#129329

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 ourterms of service andprivacy statement. We’ll occasionally send you account related emails.

Already on GitHub?Sign in to your account

Merged
pablogsal merged 18 commits intopython:mainfromtomasr8:completer
Apr 25, 2025

Conversation

tomasr8
Copy link
Member

@tomasr8tomasr8 commentedJan 27, 2025
edited
Loading

DPO discussion thread:https://discuss.python.org/t/looking-for-feedback-on-adding-import-autocomplete-to-pyrepl/82281

Adds a module autocomplete functionality to the PyREPL. Some examples first:

import<tab>importfoo<tab>importfoo.<tab>importfooasbar,baz<tab>from<tab>fromfoo<tab>fromfooimport<tab>fromfooimportbar<tab>fromfooimport (barasbaz,qux<tab>

(Check the tests to see a complete list of what is and is not supported)

The implementation is based on the original patch by@vadmium and adapted to PyREPL.

It can autcomplete bothimport andfrom ... import statements as long as the import fragment is valid. It uses a tokenizer/parser-based approach so that it can correctly recognize more complex contructs such asfrom foo import (bar as baz, qux<tab>.

Module search is done bypkgutil.iter_modules.

I wasn't sure where exactly to put this - for now it hooks intoReadlineAlikeReader.get_completions, let me know if there's a better place for it!

Note that, if there are any import completions available, normal completions are turned off (so that e.g.import m<tab> will not suggestimport map().

Feedback welcome :)

pllim reacted with thumbs up emojidanielhollas, hugovk, Fidget-Spinner, donBarbos, and serban reacted with rocket emoji
@picnixz
Copy link
Member

I have a branch which adds a filtering logic to the completer (seehttps://discuss.python.org/t/repl-introduce-generic-filters-to-filter-auto-completion-matches/77553) and I wondered whether we could expand the Completer interface in order to have a more generic hook category. Both your work and mine can be orthogonal but we might take this opportunity to make the completer class more generic (I haven't looked at the PR in details as I'm travelling now)

@tomasr8
Copy link
MemberAuthor

Right, I remember seeing your dpo post before! I think it's a good idea :) With this PR I decided to target PyREPL since it is less stable than rlcompleter and thus it's easier to expand/make changes. Though we could definitely think about unifying the interface with that of rlcompleter. For now the API is a bit ad hoc so I'd welcome some discussion in that direction 🙂

@@ -161,6 +170,11 @@ def get_completions(self, stem: str) -> list[str]:
result.sort()
return result

def get_module_completions(self) -> list[str]:
completer = ModuleCompleter(namespace={'__package__': '_pyrepl'}) # TODO: namespace?
Copy link
MemberAuthor

Choose a reason for hiding this comment

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

One thing I wasn't sure about is how to handle relative imports in regards to__package__. In PyREPL, it is set to_pyrepl so I replicate that here but maybe we want it to be configurable?

@hugovk
Copy link
Member

  1. On macOS, typeimport re<tab>:
>>>import re[ complete but not unique ]
  1. Press<tab> again:
>>>import rere        reprlib   readline  resource  requests
  1. Then change your mind and pressctrl+C, and you get:
>>>import reKeyboardInterrupt   readline  resource  requests>>>re        reprlib   readline  resource  requests

I'd expect something more like:

>>> import reKeyboardInterrupt>>>
  1. Pressctrl+C a couple more times:
>>>import reKeyboardInterrupt   readline  resource  requests>>>KeyboardInterrupt   readline  resource  requests>>>KeyboardInterrupt   readline  resource  requests>>>re        reprlib   readline  resource  requests

I'd expect something more like:

>>>import reKeyboardInterrupt>>>KeyboardInterrupt>>>KeyboardInterrupt>>>

@tomasr8
Copy link
MemberAuthor

Answering here what I wrote on Discord - thectrl+C behaviour is present for the existing name/attribute autocomplete as well, this PR is not changing that. I think it's something we could add in a separate PR though!

hugovk reacted with thumbs up emoji

@tomasr8tomasr8 added the topic-replRelated to the interactive shell labelFeb 22, 2025
@gaogaotiantian
Copy link
Member

I only did a quick look at the code. Are you trying to import the module during completion?

@tomasr8
Copy link
MemberAuthor

I only did a quick look at the code. Are you trying to import the module during completion?

Yes I try to import it to ensure it's a valid package and so that I can easily list the submodules (e.g.import foo.<tab>). IPython doessomething similar. Is it a concern?

@gaogaotiantian
Copy link
Member

Yes I try to import it to ensure it's a valid package and so that I can easily list the submodules (e.g.import foo.<tab>). IPython doessomething similar. Is it a concern?

I think so. Implicitly import a module during auto-completion seems a bit unintentional to me. For example, importingtorch could take 2~3 seconds, and to user it would be a freeze during completion. Some libraries monkeypatch stdlib or even Python during import, and that's some hidden side effect. Personally I don't like this implicit behavior. Also, will the import only triggered if. is in the text? Or for all possible completion? Say I doimport t<tab> does it import all the modules starting with at?

dolfinus reacted with thumbs up emoji

@tomasr8
Copy link
MemberAuthor

Say I do import t does it import all the modules starting with a t?

This PR usespkgutil.iter_modules so just typingimport t<tab> does not actually import anything. The results ofpkgutil.iter_modules are also cached so the top-level packages are only searched once.

Ido import in case you typeimport torch.x<tab>, but I think I might be able to avoid that.

What I think I cannot avoid is an import in case you typefrom foo import x<tab>. In order for me to know which objects are available for import, I need to actually import the module. I don't think there is a way around that? What do you think?

@gaogaotiantian
Copy link
Member

Personally, I would prefer if the auto-completernever imports any module without my knowledge, even if that means it can't do its job in some cases. It's okay if it completes when the module already imported, but importing a new module during auto-completion seems wrong to me. I think we should at least get opinions from a few other core devs about this, because in my mind this is a relatively large new behavior.

tomasr8 reacted with thumbs up emoji

@tomasr8
Copy link
MemberAuthor

I think we should at least get opinions from a few other core devs about this, because in my mind this is a relatively large new behavior.

I admit I do not fully understand the implications of this so I think getting more opinions is a good idea :) Do you want me to start a discussion on Discord?

@gaogaotiantian
Copy link
Member

Discord or/and dpo maybe? We should at least get some opinions from repl owners.

tomasr8 reacted with thumbs up emoji

@tomasr8
Copy link
MemberAuthor

ok! It's getting a bit late here, so I'll do it tomorrow 🙂

@tomasr8
Copy link
MemberAuthor

I updated the PR following the discussion on DPO:https://discuss.python.org/t/looking-for-feedback-on-adding-import-autocomplete-to-pyrepl/82281

  • Modules are no longer imported. Modules are discovered using a combination ofpkgutil.iter_modules andsubmodule_search_locations.
  • Removed support for module attributes. This relied on actually importing the modules. We can add this once we have decided in which way we should discover module attributes.
hugovk reacted with thumbs up emoji

@@ -161,6 +169,11 @@ def get_completions(self, stem: str) -> list[str]:
result.sort()
return result

def get_module_completions(self) -> list[str]:
completer = ModuleCompleter(namespace={'__package__': '_pyrepl'}) # TODO: namespace?
Copy link
Member

Choose a reason for hiding this comment

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

Either remove the TODO or we should handle this differently

Copy link
MemberAuthor

Choose a reason for hiding this comment

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

I removed the TODO and left a comment. Inside pyrepl,__package__ is set to_pyrepl so the module completer should use the same value when resolving relative packages.

def get_module_completions(self) -> list[str]:
completer = ModuleCompleter(namespace={'__package__': '_pyrepl'}) # TODO: namespace?
line = self.get_line()
return completer.get_completions(line)
Copy link
Member

Choose a reason for hiding this comment

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

Can this raise? What do we want to do if any of the pkgutil raises for any reason?

Copy link
MemberAuthor

Choose a reason for hiding this comment

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

It shouldn't unlessiter_modules orfind_spec raise. Since those can call code from user-supplied finders, they can raise an arbitrary exception. I could add a simpleexcept Exception block and simply return no completions if that happens. To the user it would look like no completions are available rather than outright crashing the repl.

Copy link
MemberAuthor

Choose a reason for hiding this comment

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

Added infd81999

"""Global module cache"""
if not self._global_cache or self._curr_sys_path != sys.path:
self._curr_sys_path = sys.path[:]
self._global_cache = list(pkgutil.iter_modules())
Copy link
Member

Choose a reason for hiding this comment

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

Nothing to do here but I am a bit concerned about how this can perform for a lot of packages

Copy link
MemberAuthor

Choose a reason for hiding this comment

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

See this#129329 (comment) with timings

Comment on lines 821 to 824
if self.code.rstrip().endswith('import') and self.code.endswith(' '):
return Result(from_name=self.parse_empty_from_import(), name='')
if self.code.rstrip().endswith('from') and self.code.endswith(' '):
return Result(from_name='')
Copy link
Member

Choose a reason for hiding this comment

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

This callscode.rstrip() a lot, maybe is worth setting that as an attribute or just always useself.code = code.rstrip(). What do you think?

At the very least maybe set it as a local so you don't strip twice

Copy link
MemberAuthor

Choose a reason for hiding this comment

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

I added it as a local since it's only called in 3 places

@@ -161,6 +169,11 @@ def get_completions(self, stem: str) -> list[str]:
result.sort()
return result

def get_module_completions(self) -> list[str]:
completer = ModuleCompleter(namespace={'__package__': '_pyrepl'}) # TODO: namespace?
Copy link
Member

Choose a reason for hiding this comment

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

Is this creating one of this guys per request? Should we cache theModuleCompleter?

Copy link
MemberAuthor

Choose a reason for hiding this comment

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

it is, and yes we should cache it. Updated in5c11124 (I also moved the completer to a separate file to avoid having to reorder everything when I added it to theReadlineConfig)

Copy link
MemberAuthor

@tomasr8tomasr8Apr 19, 2025
edited
Loading

Choose a reason for hiding this comment

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

Also changed inf4e290a so that each Reader gets a newModuleCompleter instance (with its own module cache)

@pablogsal
Copy link
Member

@tomasr8 can you try to play a bit with some edge cases (tons of packages, etc) to see how the performance is for extreme circumstances? I want to be aware of the ways this can "break"

@tomasr8
Copy link
MemberAuthor

tomasr8 commentedApr 19, 2025
edited
Loading

Thanks for taking the time to review this, I really appreciate it!

@tomasr8 can you try to play a bit with some edge cases (tons of packages, etc) to see how the performance is for extreme circumstances? I want to be aware of the ways this can "break"

When it comes to performance, the biggest bottleneck is the call topkgutil.iter_modules() to find all top-level packages.
(the result is cached so we pay the cost only the first time you hit TAB).

Here are some timings for lots of packages. I installed the top 1000 pypi packages (at least those that can be installed on 3.14 so 836 packages installed):

~ pip freeze| wc -l836

The timings are on a laptop with Intel ultra 9 185H CPU and a new-ish SSD (relevant becausepkgutil.iter_modules interacts with the file system).

For those ~800 packages in a single location,pkgutil.iter_modules takes about 0.03s:

>>>importtime,pkgutil>>>start=time.time();list(pkgutil.iter_modules());time.time()-start0.03072071075439453

Typingimport <tab> takes about 0.03s as well so most of the cost is really insidepkgutil.iter_modules:

>>>import<tab>0.03162884712219238

Subsequent completion requests are much faster since the results are cached:

>>>import<tab>0.002469778060913086

Another thing I tried is multiple search locations (i.e. multiplesys.path entries):

5 different locations and ~4000 packages total:

`pkgutil.iter_modules`:0.3535459041595459sInitial`import <tab>`:0.3592982292175293sSubsequent`import <tab>`:0.0021576881408691406s

10 different locations and ~8000 packages total:

`pkgutil.iter_modules`:0.6836235523223877sInitial`import <tab>`:0.6652779579162598sSubsequent`import <tab>`:0.0002522468566894531s

The initial search is about 0.35s and 0.68s respectively, while the subsequent ones are again very cheap.

@tomasr8tomasr8 requested a review frompablogsalApril 20, 2025 18:48
@hugovk
Copy link
Member

I still get this failure locally:

======================================================================FAIL:test_import_completions (test.test_pyrepl.test_pyrepl.TestPyReplModuleCompleter.test_import_completions) (code='import path\t\n')----------------------------------------------------------------------Traceback (most recent call last):  File"/Users/hugo/github/python/cpython/main/Lib/test/test_pyrepl/test_pyrepl.py", line938, intest_import_completionsself.assertEqual(output, expected)~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^AssertionError:'import path' != 'import pathlib'- import path+ import pathlib?            +++

It's because my env isn't a clean slate because I've pip installed some packages for testing on 3.14, and a dependency is interfering:

Python 3.14.0a7+ (heads/completer:f4e290a03f1, Apr 23 2025, 13:32:14) [Clang 16.0.0 (clang-1600.0.26.6)] on darwinType "help", "copyright", "credits" or "license" for more information.>>>import path<tab>pathlib       pathvalidate
./python.exe -m pip freeze| grep pathpathvalidate==3.2.0

I don't think this needs to block initial merge before the freeze, but would be good to fix.

(PS there's a merge conflict on some imports.)

@tomasr8
Copy link
MemberAuthor

Weird, I tested this locally with some scripts onsys.path and it passed for me. Sorry about that, I'll take a look later today!

hugovk reacted with thumbs up emoji

@tomasr8
Copy link
MemberAuthor

Ok soiter_modules also gets FileFinders fromsys.path so I had to change the approach but itshould work now. Tested locally withpathvalidate installed and the tests are passing.

@hugovk
Copy link
Member

Passing now, thanks!

tomasr8 reacted with rocket emoji

@pablogsalpablogsal merged commitc3a7118 intopython:mainApr 25, 2025
51 checks passed
@pablogsal
Copy link
Member

LGTM, fantastic job@tomasr8

hugovk reacted with hooray emojitomasr8 reacted with heart emoji

Sign up for freeto join this conversation on GitHub. Already have an account?Sign in to comment
Reviewers

@hugovkhugovkhugovk left review comments

@pablogsalpablogsalpablogsal approved these changes

@lysnikolaoulysnikolaouAwaiting requested review from lysnikolaoulysnikolaou is a code owner

@ambvambvAwaiting requested review from ambvambv is a code owner

Assignees
No one assigned
Labels
topic-replRelated to the interactive shell
Projects
None yet
Milestone
No milestone
Development

Successfully merging this pull request may close these issues.

5 participants
@tomasr8@picnixz@hugovk@gaogaotiantian@pablogsal

[8]ページ先頭

©2009-2025 Movatter.jp