-
Notifications
You must be signed in to change notification settings - Fork 119
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
Make IPython optional #125
Conversation
Codecov Report
@@ Coverage Diff @@
## main #125 +/- ##
==========================================
- Coverage 53.96% 48.81% -5.15%
==========================================
Files 4 5 +1
Lines 378 381 +3
Branches 58 58
==========================================
- Hits 204 186 -18
- Misses 156 172 +16
- Partials 18 23 +5
Continue to review full report at Codecov.
|
I'm in favor of this change. At the very least, separating the IPython plugin from the core functionality is a positive step. Making it completely optional - IMO - is even better. But this is a widely used library, so let's be careful. Based on a quick google search, I don't see any other projects that are using
I have a few more comments. I'll leave them inline. |
line_profiler/line_profiler.py
Outdated
from IPython.utils.ipstruct import Struct | ||
from IPython.core.error import UsageError | ||
try: | ||
from ipython_extension import load_ipython_extension |
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.
Should, at the very least, be from .ipython_extension import load_ipython_extension
However, this is a circular import, and while that can be ok, I'd rather avoid it for code readability. If I understand correctly, the important thing is that there is a load_ipython_extension
function exposed at the top level.
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.
Indeed, I forgot the leading dot, thanks for noticing.
I removed the circular import by not importing load_ipython_extension
in line_profiler.py
at all.
@@ -155,6 +155,8 @@ def add_module(self, mod): | |||
return nfuncsadded | |||
|
|||
|
|||
# This could be in the ipython_extension submodule, | |||
# but it doesn't depend on the IPython module so it's easier to just let it stay here. |
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.
I agree, this is just handling the case where we are printing something in a notebook. It doesn't have to do with the plugin directly, so it's fine to stay here.
line_profiler/__init__.py
Outdated
'load_ipython_extension', 'load_stats', 'main', 'show_func', | ||
'show_text', '__version__'] | ||
|
||
try: | ||
from .ipython_extension import LineProfilerMagics |
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.
In the else
you can simply set LineProfilerMagics = None
to avoid having to dynamically modify __all__
(I prefer it when __all__
is static). We can do the same for the load_ipython_extension
function, but maybe it becomes a stub function that raises an error if IPython is not available?
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.
I changed the way LineProfilerMagics
is defined and added a lazy import. I still think LineProfilerMagics
should be removed from __init__.py
as an implementation detail, but I understand your concern about potential backward compatibility.
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.
I would be ok with removing this from the top level API. It doesn't really belong there, and again I doubt anyone is using it. It might be good to search for any potential usage over github and other OSS code repos just in case though. If it doesn't look like it is in use, let's get rid of it if that's easier.
163f8fd
to
cf4fd38
Compare
cf4fd38
to
adfc2cb
Compare
With the last modifications, |
line_profiler/__init__.py
Outdated
show_func, show_text,) | ||
|
||
from .ipython_extension import load_ipython_extension, LineProfilerMagics |
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.
This won't work with lazy import, at this point LineProfilerMagics
is always None
, and it won't change when the value is updated in ipython_extension
.
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.
Will it be the case that we only care if load_ipython_extension
is defined if IPython
is in sys.modules
? That could be a potential optimization that prevents the large import in cases where it is not needed.
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 idea, it will be cleaner this way and should work with you other suggestion below.
line_profiler/ipython_extension.py
Outdated
""" API for IPython to recognize this module as an IPython extension. | ||
""" | ||
|
||
# Import IPython inside the function to load IPython only if necessary |
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.
I definitely don't want to define this class as a local object. That can cause weird corner cases on win32 where objects need to be defined in the global scope for them to be pickleable, and hence shared across a multiprocessing queue. An easy workaround is to make an _ipython_extension_backend.py
module (If the ipython_extension.py
module can't serve that purpose) that defines the classes of interest, but is only conditionally imported.
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.
I see what you mean, I'll try something like this.
def load_ipython_extension(ip): | ||
""" API for IPython to recognize this module as an IPython extension. | ||
""" | ||
from .ipython_extension import LineProfilerMagics |
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.
It can either be imported from this function, or it can be added with the other imports with a sys.modules guard for IPython.
In the second case there would be a circular import but it shouldn't be a problem.
There should probably be some tests to check the behavior with and without IPython but I'm not sure how to do it. |
Tests for a larger change like this would be ideal. I found a few articles that look like they go through how to test ipython magics: https://pmbaumgartner.github.io/blog/testing-ipython-magics/ https://medium.com/@davide.sarra/how-to-test-magics-in-ipython-extensions-86d99e5d6802 |
Thanks to the links you provided I added a test for IPython. |
There has been problems with this repo and coverage. I'm not sure of the ultimate culprits either, but that's a known issue and does not need to be part of this PR. Right now it LGTM, I'll do a final more careful review in the next few days and then merge. |
Thanks @Nodd, this LGTM! This reduces import time from |
Nice ! |
The only runtime dependency is IPython, which needs many other libraries as well. This make a heavy installation for a features that is completely optional.
It was raised before in rkern/line_profiler#112, with some PR that were never merged, apparently due to lack of time: rkern/line_profiler#139, rkern/line_profiler#114. There is also comments from @rkern that it should be optional: rkern/line_profiler#90
This PR is my take on the subject, since I don't use ipython I had the motivation to get rid of it. It is still an optional dependency for pip, to be able to check the version:
pip isntall line_profiler[ipython]
I think that the approach in rkern/line_profiler#139 is better: the IPython import is only made in the
load_ipython_extension
call. But sinceLineProfilerMagics
is added to the "public API" in__init__.py
I couldn't use it easily. The question is, can it be removed or is it needed somewhere ?