-
-
Notifications
You must be signed in to change notification settings - Fork 892
Do CLI import in __main__, not __init__ #3547
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
base: master
Are you sure you want to change the base?
Conversation
88baca0
to
cccc8cb
Compare
@@ -59,7 +44,6 @@ def main() -> None: # type: ignore | |||
"InvalidURL", | |||
"Limits", | |||
"LocalProtocolError", | |||
"main", |
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 assume this causes breaking change (deletion from public api) we may need to keep it as pointer to __main__.main
which also lazy import it:
def main():
from .__main__ import main as orignial_main
original_main()
by this way, there is no need to update test.
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.
good point, will make this 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.
Turns out this is a little more complicated, unfortunately. The main
function in httpx/_main.py
is a click.BaseCommand
, not just a typing.Callable
. Wrapping it in a new def main(...)
still changes the public API, since httpx.main
is no longer a click.BaseCommand
. This gets exposed by the tests, which attempt to access attributes of a click.BaseCommand
object that aren't there for a simple function. To address, I believe we'd have to have httpx.__init__.py
import click
, which leads to the issue we're trying to get around in the first place, or we'd have to do some funky mocking magic to make main
look more like a click.BaseCommand
. I'm happy to explore that if that's the direction you'd like this to head, but doesn't feel quite right to me at this point.
In summary, I unfortunately can't think of a good way right now to maintain the current public API and achieve the goal of this PR.
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.
Wrapping it in a new
def main(...)
still changes the public API
Also, it could use PEP562 (module level __getattr__
) instead of wrapping:
def __getattr__(name):
if name == "main":
from .__main__ import main
return main
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
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.
Thanks, updated. Removed the __main__.py
module to keep the diff as small as possible. Can add back, or it can go in a separate PR since it's a separate feature.
28a382b
to
fab0285
Compare
Importing the CLI code doubles the import time for httpx altogether. By importing lazily at runtime, the additional time only happens when running the httpx CLI, and not on every "import httpx".
Summary
From #3523
Importing the
_main.py
module inhttpx/__init__.py
adds 0.087s worth of import time on my machine (MacBook M1 Pro, 32 GB RAM, macOS 15.3.1) running Python 3.13.2.It's certainly not a huge amount of time in the absolute, but it roughly doubles the amount of time it takes to
import httpx
, even when you're not intending to run the CLI. Here's an import time graph generated using tuna on the output ofpython -X importtime -c "import httpx" > import.log
This PR adds
httpx/__main__.py
that does the import instead. This also allowspython -m httpx
as in #3216.It does change the public API:
httpx.main
becomes the (more ungainly)httpx.__main__.main
.Checklist