GeezerVisonThe World Wide Web, restored to 800 pixels of glory. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Home|About|Gaming cabinet|Archives|How it works|Starter sites | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
GeezerVison is showing github.com — gh-69605: Add module autocomplete to PyREPL by tomasr8 · Pull Request #129329 · python/cpython Conversation
|
|
I have a branch which adds a filtering logic to the completer (see https://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) |
Sorry, something went wrong.
There was an error while loading. Please reload this page.
|
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 🙂 |
Sorry, something went wrong.
There was an error while loading. Please reload this page.
| return result | ||
|
|
||
| def get_module_completions(self) -> list[str]: | ||
| completer = ModuleCompleter(namespace={'__package__': '_pyrepl'}) # TODO: namespace? |
There was a problem 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?
Sorry, something went wrong.
There was an error while loading. Please reload this page.
>>> import re
[ complete but not unique ]
>>> import re
re reprlib readline resource requests
>>> import re
KeyboardInterrupt readline resource requests
>>>
re reprlib readline resource requestsI'd expect something more like:
>>> import re
KeyboardInterrupt readline resource requests
>>>
KeyboardInterrupt readline resource requests
>>>
KeyboardInterrupt readline resource requests
>>>
re reprlib readline resource requestsI'd expect something more like: >>> import re
KeyboardInterrupt
>>>
KeyboardInterrupt
>>>
KeyboardInterrupt
>>> |
Sorry, something went wrong.
There was an error while loading. Please reload this page.
|
Answering here what I wrote on Discord - the |
Sorry, something went wrong.
There was an error while loading. Please reload this page.
|
I only did a quick look at the code. Are you trying to import the module during completion? |
Sorry, something went wrong.
There was an error while loading. Please reload this page.
Yes I try to import it to ensure it's a valid package and so that I can easily list the submodules (e.g. |
Sorry, something went wrong.
There was an error while loading. Please reload this page.
There was an error while loading. Please reload this page.
There was an error while loading. Please reload this page.
There was an error while loading. Please reload this page.
There was an error while loading. Please reload this page.
There was an error while loading. Please reload this page.
There was an error while loading. Please reload this page.
There was an error while loading. Please reload this page.
There was an error while loading. Please reload this page.
There was an error while loading. Please reload this page.
There was an error while loading. Please reload this page.
I think so. Implicitly import a module during auto-completion seems a bit unintentional to me. For example, importing |
Sorry, something went wrong.
There was an error while loading. Please reload this page.
This PR uses I do import in case you type What I think I cannot avoid is an import in case you type |
Sorry, something went wrong.
There was an error while loading. Please reload this page.
|
Personally, I would prefer if the auto-completer never 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. |
Sorry, something went wrong.
There was an error while loading. Please reload this page.
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? |
Sorry, something went wrong.
There was an error while loading. Please reload this page.
|
Discord or/and dpo maybe? We should at least get some opinions from repl owners. |
Sorry, something went wrong.
There was an error while loading. Please reload this page.
|
ok! It's getting a bit late here, so I'll do it tomorrow 🙂 |
Sorry, something went wrong.
There was an error while loading. Please reload this page.
|
I updated the PR following the discussion on DPO: https://discuss.python.org/t/looking-for-feedback-on-adding-import-autocomplete-to-pyrepl/82281
|
Sorry, something went wrong.
There was an error while loading. Please reload this page.
| return result | ||
|
|
||
| def get_module_completions(self) -> list[str]: | ||
| completer = ModuleCompleter(namespace={'__package__': '_pyrepl'}) # TODO: namespace? |
There was a problem 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
Sorry, something went wrong.
There was an error while loading. Please reload this page.
There was a problem 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.
Sorry, something went wrong.
There was an error while loading. Please reload this page.
| def get_module_completions(self) -> list[str]: | ||
| completer = ModuleCompleter(namespace={'__package__': '_pyrepl'}) # TODO: namespace? | ||
| line = self.get_line() | ||
| return completer.get_completions(line) |
There was a problem 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?
Sorry, something went wrong.
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
The reason will be displayed to describe this comment to others. Learn more.
It shouldn't unless iter_modules or find_spec raise. Since those can call code from user-supplied finders, they can raise an arbitrary exception. I could add a simple except 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.
Sorry, something went wrong.
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
The reason will be displayed to describe this comment to others. Learn more.
Added in fd81999
Sorry, something went wrong.
There was an error while loading. Please reload this page.
| """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()) |
There was a problem 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
Sorry, something went wrong.
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
The reason will be displayed to describe this comment to others. Learn more.
See this #129329 (comment) with timings
Sorry, something went wrong.
There was an error while loading. Please reload this page.
| 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='') |
There was a problem hiding this comment.
The reason will be displayed to describe this comment to others. Learn more.
This calls code.rstrip() a lot, maybe is worth setting that as an attribute or just always use self.code = code.rstrip(). What do you think?
At the very least maybe set it as a local so you don't strip twice
Sorry, something went wrong.
There was an error while loading. Please reload this page.
There was a problem 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
Sorry, something went wrong.
There was an error while loading. Please reload this page.
| return result | ||
|
|
||
| def get_module_completions(self) -> list[str]: | ||
| completer = ModuleCompleter(namespace={'__package__': '_pyrepl'}) # TODO: namespace? |
There was a problem 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 the ModuleCompleter?
Sorry, something went wrong.
There was an error while loading. Please reload this page.
There was a problem 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 in 5c11124 (I also moved the completer to a separate file to avoid having to reorder everything when I added it to the ReadlineConfig)
Sorry, something went wrong.
There was an error while loading. Please reload this page.
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
The reason will be displayed to describe this comment to others. Learn more.
Also changed in f4e290a so that each Reader gets a new ModuleCompleter instance (with its own module cache)
Sorry, something went wrong.
There was an error while loading. Please reload this page.
|
@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" |
Sorry, something went wrong.
There was an error while loading. Please reload this page.
There was an error while loading. Please reload this page.
|
Thanks for taking the time to review this, I really appreciate it!
When it comes to performance, the biggest bottleneck is the call to 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 -l 836 The timings are on a laptop with Intel ultra 9 185H CPU and a new-ish SSD (relevant because For those ~800 packages in a single location, >>> import time, pkgutil >>> start = time.time(); list(pkgutil.iter_modules()); time.time() - start 0.03072071075439453 Typing >>> 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. multiple 5 different locations and ~4000 packages total: `pkgutil.iter_modules`: 0.3535459041595459s Initial `import <tab>`: 0.3592982292175293s Subsequent `import <tab>`: 0.0021576881408691406s 10 different locations and ~8000 packages total: `pkgutil.iter_modules`: 0.6836235523223877s Initial `import <tab>`: 0.6652779579162598s Subsequent `import <tab>`: 0.0002522468566894531s The initial search is about 0.35s and 0.68s respectively, while the subsequent ones are again very cheap. |
Sorry, something went wrong.
There was an error while loading. Please reload this page.
|
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", line 938, in test_import_completions self.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 darwin Type "help", "copyright", "credits" or "license" for more information. >>> import path<tab> pathlib pathvalidate ❯ ./python.exe -m pip freeze | grep path pathvalidate==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.) |
Sorry, something went wrong.
There was an error while loading. Please reload this page.
|
Weird, I tested this locally with some scripts on |
Sorry, something went wrong.
There was an error while loading. Please reload this page.
|
Ok so |
Sorry, something went wrong.
There was an error while loading. Please reload this page.
|
Passing now, thanks! |
Sorry, something went wrong.
There was an error while loading. Please reload this page.
|
LGTM, fantastic job @tomasr8 |
Sorry, something went wrong.
There was an error while loading. Please reload this page.
There was an error while loading. Please reload this page.
Successfully merging this pull request may close these issues.
There was an error while loading. Please reload this page.