-
-
Notifications
You must be signed in to change notification settings - Fork 656
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
Faster Auto-completion #231
Comments
with your proposed api, typer would still need to import main.py in order to get the list of commands and completion functions, which means it would still need to import all the modules and packages imported in main.py, and all of those packages' dependencies, and so on I don't see how that would improve performance. One possible workaround would be to refactor your cli entrypoint module ( i.e. instead of: ## main.py
from typer import Typer, Argument
import rich.console # not necessary
import pystray # not necessary
import pynput # not necessary
import watchgod # not necessary
app = Typer()
def some_command_autocompletion():
# do completion
pass
@app.command()
def some_command(name: str = Argument(autocompletion=some_command_autocompletion)):
# do stuff
pass you could maybe do something like: ## main.py
from typer import Typer, Argument
app = Typer()
def some_command_autocompletion():
# do completion
pass
@app.command()
def some_command(name: str = Argument(autocompletion=some_command_autocompletion)):
import rich.console # not necessary
import pystray # not necessary
import pynput # not necessary
import watchgod # not necessary
# do stuff
pass |
Thanks for reply. This approach makes sense but sometimes just to type hint the code we'll have to make global import. def press_key(key: Union[str, pynput.Keyboard.key]):
.... This will require me to have |
I see... So with your proposed API, typer would parse and cache all commands and their completion functions only once, when How would you manage that cache? how would typer know when that cache needs to be invalidated? |
This is kind of what I have in mind. # psuedo structure inside shell completion file.
commands = {
"git": ["branch", "commit"],
"git branch": ["function:branch_list"],
"git commit": ["-m"],
} We store command names and completion function names. While completion, if it's a function name then get completion values by executing module |
As a quick hack, I did the following in my application: Given this layout: skm_cli/_cli
├── __init__.py
├── _agent.py
├── _aws.py
├── _cli.py
├── _dmarc.py
├── _project.py
├── _publish.py
├── _slack.py
├── _tf.py
└── _utils.py
import typer
from skm_cli._cli import _agent
from skm_cli._cli import _aws
from skm_cli._cli import _dmarc
from skm_cli._cli import _middleware
from skm_cli._cli import _project
from skm_cli._cli import _publish
from skm_cli._cli import _slack
from skm_cli._cli import _tf
app = typer.Typer(
no_args_is_help=True,
pretty_exceptions_enable=False,
)
app.add_typer(_agent.app, name="agent")
app.add_typer(_aws.app, name="aws")
app.add_typer(_dmarc.app, name="dmarc")
app.add_typer(_publish.app, name="publish")
app.add_typer(_project.app, name="project")
app.add_typer(_slack.app, name="slack")
app.add_typer(_tf.app, name="tf")
import sys
import os
def should_define(command: str) -> bool:
return _cli_is_invoking_command(command=command) or _autocomplete_is_resolving_command(command=command)
def _cli_is_invoking_command(command: str) -> bool:
return command in sys.argv
def _autocomplete_is_resolving_command(command: str) -> bool:
return command in os.environ.get("_TYPER_COMPLETE_ARGS", "")
import typer
from skm_cli._cli import _utils
app = typer.Typer(no_args_is_help=True)
if _utils.should_define(command="dmarc"):
... # all commands are defined below The end result is - all cli commands are lazily defined. Given some heavy imports, my app autocompletes would clock it at around 0.5s. With this hack, they're down to 0.1s |
Pardon me for my ignorance @sidekick-eimantas : what is your benefit in prefixing all modules with an underscore ? |
Hey Nikos This is a convention we use, which is an extension of https://docs.python.org/3/tutorial/classes.html#private-variables, applied to modules. We define the interface for the # CLI Package
# The only interface here is `app`
# Call the `app` method to hand off control to the CLI.
from skm_cli.cli._cli import app as app The underscore signals to the consumer of the package not to import the modules directly, but instead to look at the It's tedious and I don't recommend it for small codebases |
Thank you @sidekick-eimantas . What are then (other?) benefits other than safeguarding the 'consumer' from no meaningful import(s, is my guess) ? I do have a somewhat complex use-case here, so maybe I am a candidate to replicate your approach ? |
It's primarily just a means of documentation. Akin to exports in other languages like Erlang. Rather than having to look through the code of large modules or read external documentation to understand what are the interfaces of a module/package, the consumer only has to look at Hope that helps. |
That gives an idea. Thank you for your invaluable time to respond. |
Thank you @sidekick-eimantas , that's a very nice architecture somewhat similar to my approach, in that modules and packages are only imported when needed. It's a very nice workaround, but i wish there was a more universal solution... @tiangolo, what do you think about typer looking for and introspecting *..pyi files? My thinking here is that when performing autocomplete, typer doesn't rerally need to import / process function bodies and all their respective dependencies, it only needs the function signatures in order to perform autocomplete. it would be awesome if typer could (during autocomplete) pull these signatures from .pyi files instead of importing and introspecting .py modules themselves, and in so doing would massively cut down the time it takes to auto-complete tasks if typer is asked to autocomplete a function argument that has a defined completetion function in its signature, typer would still need to import that specific module and execute that function, but other than that, we'd see a huge increase in completion performance / responsiveness i think. Another benefit of leveraging .pyi files for this is that we wouldn't have to reinvent the wheel. There are already several existing dev tools which allow us to generate .pyi files for the code in our typer-powered codebases :) |
I've had another look at this thread, a year wiser since last I replied with a suggestion, and am wondering if as a quick potential fix for this, import typer
app = typer.Typer(
no_args_is_help=True,
pretty_exceptions_enable=False,
)
app.add_typer("skm_cli._cli._agent:app", name="agent")
app.add_typer("skm_cli._cli._aws.app:app", name="aws") |
Click supports the concept of a I like your suggestion @sidekick-eimantas. As you mention it's a familiar pattern from uvicorn. |
Autocompleting the commands was insanely slow. Putting imports inside the commands helped. I got the idea from this issue: fastapi/typer#231
Since 2021 speed issue now is not just due to custom code / imports, but typer itself became quite slow, for instance just running import like this
takes ~200ms (!) on high-end ubuntu 22.04 server with python3.11 with typer==0.12, while typer 0.9 takes ~85ms (and 40 ms is python startup time). It is ~20 percent faster on mac, but story is the same. This affects autosuggestion experience of course. |
I'm also seeing very long load times for the typer module on Windows, anywhere between .2 and .5 seconds, making the autocomplete process very sluggish. from time import time
now = time()
import typer
print(f"Imported in {time()-now}s")
|
I'm seeing similar speed issues on windows:
|
The biggest culprit of slow typer import time seems to be (this is on OSX but similar graph on Linux) Also see this discussion where @JPHutchins has addressed some of this upstream in Rich. Problem is their fix is merged but rich hasn't cut a release in 6+ months... |
@iamthebot What is the tool behind the time-profiling ? |
It’s pretty awesome pip install tuna
python -X importtime <my script/module> 2>importtime.log
tuna importtime.log |
Is your feature request related to a problem
Typer auto-completion becomes slower as the project grows. In my sample application, having only a few imports increases the response time above 100ms. This affects the user experience.
The solution you would like
If there could be some way of defining auto-completion functions in different file, we can only have imports necessary for auto-completion.
I provide a proposal below, but I am not too confident in it due to type-hint discontinuity. This just serves as starting point.
Additional context
The text was updated successfully, but these errors were encountered: