Skip to content
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

Merged
merged 12 commits into from
Mar 17, 2022
Merged

Make IPython optional #125

merged 12 commits into from
Mar 17, 2022

Conversation

Nodd
Copy link
Contributor

@Nodd Nodd commented Feb 20, 2022

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 since LineProfilerMagics 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 ?

@codecov
Copy link

codecov bot commented Feb 20, 2022

Codecov Report

Merging #125 (007efb7) into main (94147b3) will decrease coverage by 5.14%.
The diff coverage is 45.94%.

Impacted file tree graph

@@            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     
Impacted Files Coverage Δ
line_profiler/ipython_extension.py 42.02% <42.02%> (ø)
line_profiler/__init__.py 100.00% <100.00%> (ø)
line_profiler/line_profiler.py 54.43% <100.00%> (+12.91%) ⬆️
kernprof.py 43.06% <0.00%> (-30.47%) ⬇️

Continue to review full report at Codecov.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update 94147b3...007efb7. Read the comment docs.

@Erotemic
Copy link
Member

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 LineProfilerMagics directly, even though it is technically in the top-level API. I can see two paths forward:

  1. Simply remove LineProfilerMagics from the top level API. It is unlikely that this will break anyone, and if it does they can raise an issue here, or just change where they import LineProfilerMagics from.

  2. Keep the LineProfilerMagics in the top level API. This is safer. This might be the better way to go just in case. Instead of conditionally adding LineProfilerMagics to __all__. I would recommend, setting LineProfilerMagics = None in the case where the IPython extension fails to load.

I have a few more comments. I'll leave them inline.

from IPython.utils.ipstruct import Struct
from IPython.core.error import UsageError
try:
from ipython_extension import load_ipython_extension
Copy link
Member

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.

Copy link
Contributor Author

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.
Copy link
Member

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.

'load_ipython_extension', 'load_stats', 'main', 'show_func',
'show_text', '__version__']

try:
from .ipython_extension import LineProfilerMagics
Copy link
Member

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?

Copy link
Contributor Author

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.

Copy link
Member

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.

@Nodd
Copy link
Contributor Author

Nodd commented Feb 21, 2022

With the last modifications, IPython is not imported if it's installed but line_profiler is not used as an extension.

show_func, show_text,)

from .ipython_extension import load_ipython_extension, LineProfilerMagics
Copy link
Contributor Author

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.

Copy link
Member

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.

Copy link
Contributor Author

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.

""" API for IPython to recognize this module as an IPython extension.
"""

# Import IPython inside the function to load IPython only if necessary
Copy link
Member

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.

Copy link
Contributor Author

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
Copy link
Contributor Author

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.

@Nodd
Copy link
Contributor Author

Nodd commented Feb 21, 2022

There should probably be some tests to check the behavior with and without IPython but I'm not sure how to do it.

@Erotemic
Copy link
Member

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

@Nodd
Copy link
Contributor Author

Nodd commented Mar 5, 2022

Thanks to the links you provided I added a test for IPython.
I don't understand the coverage results for ipython_extension.py, the whole lprun() function is run (the output is tested) but the coverage is not detected after line 104. I have the same result locally. I don't understand what is happening.

@Erotemic
Copy link
Member

Erotemic commented Mar 6, 2022

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.

@Erotemic
Copy link
Member

Erotemic commented Mar 17, 2022

Thanks @Nodd, this LGTM!

This reduces import time from 168,715 to 10,893 microseconds. 10x improvement!

@Erotemic Erotemic merged commit 8c7624c into pyutils:main Mar 17, 2022
@Nodd Nodd deleted the ipython_optional branch March 20, 2022 23:36
@Nodd
Copy link
Contributor Author

Nodd commented Mar 20, 2022

Nice !
It should also speed up the unittests for lineprofilergui, since the slowest steop is the dependency installation.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants