Skip to content

Commit

Permalink
Make IPython optional (pyutils#125)
Browse files Browse the repository at this point in the history
* IPython is now optional

* Manage imports in __init__.py

* Forgot local import

* Use load_ipython_extension from line_profiler to get a useful message

* Typo

* Rework IPython plugin layout

* Update CHANGELOG

* Load IPython only when needed

* Add basic ipython test

* Fix deprecation warning

* Check output

Co-authored-by: Jon Crall <erotemic@gmail.com>
  • Loading branch information
Nodd and Erotemic committed Mar 17, 2022
1 parent 43348d4 commit 8c7624c
Show file tree
Hide file tree
Showing 11 changed files with 187 additions and 146 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ Changes
~~~~~
* FIX: #109 kernprof fails to write to stdout if stdout was replaced
* FIX: Fixes max of an empty sequence error #118
* Make IPython optional
* FIX: #100 Exception raise ZeroDivisionError

3.4.0
Expand Down
4 changes: 4 additions & 0 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,10 @@ Releases of `line_profiler` can be installed using pip::

$ pip install line_profiler

Installation while ensuring a compatible IPython version can also be installed using pip::

$ pip install line_profiler[ipython]

Source releases and any binaries can be downloaded from the PyPI link.

http://pypi.python.org/pypi/line_profiler
Expand Down
5 changes: 3 additions & 2 deletions line_profiler/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
"""
__submodules__ = [
'line_profiler',
'ipython_extension',
]

__autogen__ = """
Expand All @@ -13,10 +14,10 @@

from .line_profiler import __version__

from .line_profiler import (LineProfiler, LineProfilerMagics,
from .line_profiler import (LineProfiler,
load_ipython_extension, load_stats, main,
show_func, show_text,)

__all__ = ['LineProfiler', 'LineProfilerMagics', 'line_profiler',
__all__ = ['LineProfiler', 'line_profiler',
'load_ipython_extension', 'load_stats', 'main', 'show_func',
'show_text', '__version__']
143 changes: 143 additions & 0 deletions line_profiler/ipython_extension.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
from io import StringIO

from IPython.core.magic import Magics, magics_class, line_magic
from IPython.core.page import page
from IPython.utils.ipstruct import Struct
from IPython.core.error import UsageError

from .line_profiler import LineProfiler


@magics_class
class LineProfilerMagics(Magics):
@line_magic
def lprun(self, parameter_s=""):
""" Execute a statement under the line-by-line profiler from the
line_profiler module.
Usage:
%lprun -f func1 -f func2 <statement>
The given statement (which doesn't require quote marks) is run via the
LineProfiler. Profiling is enabled for the functions specified by the -f
options. The statistics will be shown side-by-side with the code through the
pager once the statement has completed.
Options:
-f <function>: LineProfiler only profiles functions and methods it is told
to profile. This option tells the profiler about these functions. Multiple
-f options may be used. The argument may be any expression that gives
a Python function or method object. However, one must be careful to avoid
spaces that may confuse the option parser.
-m <module>: Get all the functions/methods in a module
One or more -f or -m options are required to get any useful results.
-D <filename>: dump the raw statistics out to a pickle file on disk. The
usual extension for this is ".lprof". These statistics may be viewed later
by running line_profiler.py as a script.
-T <filename>: dump the text-formatted statistics with the code side-by-side
out to a text file.
-r: return the LineProfiler object after it has completed profiling.
-s: strip out all entries from the print-out that have zeros.
-u: specify time unit for the print-out in seconds.
"""

# Escape quote markers.
opts_def = Struct(D=[""], T=[""], f=[], m=[], u=None)
parameter_s = parameter_s.replace('"', r"\"").replace("'", r"\'")
opts, arg_str = self.parse_options(parameter_s, "rsf:m:D:T:u:", list_all=True)
opts.merge(opts_def)

global_ns = self.shell.user_global_ns
local_ns = self.shell.user_ns

# Get the requested functions.
funcs = []
for name in opts.f:
try:
funcs.append(eval(name, global_ns, local_ns))
except Exception as e:
raise UsageError(
f"Could not find module {name}.\n{e.__class__.__name__}: {e}"
)

profile = LineProfiler(*funcs)

# Get the modules, too
for modname in opts.m:
try:
mod = __import__(modname, fromlist=[""])
profile.add_module(mod)
except Exception as e:
raise UsageError(
f"Could not find module {modname}.\n{e.__class__.__name__}: {e}"
)

if opts.u is not None:
try:
output_unit = float(opts.u[0])
except Exception:
raise TypeError("Timer unit setting must be a float.")
else:
output_unit = None

# Add the profiler to the builtins for @profile.
import builtins

if "profile" in builtins.__dict__:
had_profile = True
old_profile = builtins.__dict__["profile"]
else:
had_profile = False
old_profile = None
builtins.__dict__["profile"] = profile

try:
try:
profile.runctx(arg_str, global_ns, local_ns)
message = ""
except SystemExit:
message = """*** SystemExit exception caught in code being profiled."""
except KeyboardInterrupt:
message = (
"*** KeyboardInterrupt exception caught in code being " "profiled."
)
finally:
if had_profile:
builtins.__dict__["profile"] = old_profile

# Trap text output.
stdout_trap = StringIO()
profile.print_stats(
stdout_trap, output_unit=output_unit, stripzeros="s" in opts
)
output = stdout_trap.getvalue()
output = output.rstrip()

page(output)
print(message, end="")

dump_file = opts.D[0]
if dump_file:
profile.dump_stats(dump_file)
print(f"\n*** Profile stats pickled to file {dump_file!r}. {message}")

text_file = opts.T[0]
if text_file:
pfile = open(text_file, "w")
pfile.write(output)
pfile.close()
print(f"\n*** Profile printout saved to text file {text_file!r}. {message}")

return_value = None
if "r" in opts:
return_value = profile

return return_value
151 changes: 9 additions & 142 deletions line_profiler/line_profiler.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,8 @@
import tempfile
import os
import sys
from io import StringIO
from argparse import ArgumentError, ArgumentParser

from IPython.core.magic import (Magics, magics_class, line_magic)
from IPython.core.page import page
from IPython.utils.ipstruct import Struct
from IPython.core.error import UsageError

try:
from ._line_profiler import LineProfiler as CLineProfiler
except ImportError as ex:
Expand All @@ -25,6 +19,13 @@
__version__ = '3.5.0'


def load_ipython_extension(ip):
""" API for IPython to recognize this module as an IPython extension.
"""
from .ipython_extension import LineProfilerMagics
ip.register_magics(LineProfilerMagics)


def is_coroutine(f):
return False

Expand Down Expand Up @@ -155,6 +156,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.
def is_ipython_kernel_cell(filename):
""" Return True if a filename corresponds to a Jupyter Notebook cell
"""
Expand Down Expand Up @@ -248,142 +251,6 @@ def show_text(stats, unit, output_unit=None, stream=None, stripzeros=False):
output_unit=output_unit, stream=stream,
stripzeros=stripzeros)


@magics_class
class LineProfilerMagics(Magics):

@line_magic
def lprun(self, parameter_s=''):
""" Execute a statement under the line-by-line profiler from the
line_profiler module.
Usage:
%lprun -f func1 -f func2 <statement>
The given statement (which doesn't require quote marks) is run via the
LineProfiler. Profiling is enabled for the functions specified by the -f
options. The statistics will be shown side-by-side with the code through the
pager once the statement has completed.
Options:
-f <function>: LineProfiler only profiles functions and methods it is told
to profile. This option tells the profiler about these functions. Multiple
-f options may be used. The argument may be any expression that gives
a Python function or method object. However, one must be careful to avoid
spaces that may confuse the option parser.
-m <module>: Get all the functions/methods in a module
One or more -f or -m options are required to get any useful results.
-D <filename>: dump the raw statistics out to a pickle file on disk. The
usual extension for this is ".lprof". These statistics may be viewed later
by running line_profiler.py as a script.
-T <filename>: dump the text-formatted statistics with the code side-by-side
out to a text file.
-r: return the LineProfiler object after it has completed profiling.
-s: strip out all entries from the print-out that have zeros.
-u: specify time unit for the print-out in seconds.
"""

# Escape quote markers.
opts_def = Struct(D=[''], T=[''], f=[], m=[], u=None)
parameter_s = parameter_s.replace('"', r'\"').replace("'", r"\'")
opts, arg_str = self.parse_options(parameter_s, 'rsf:m:D:T:u:', list_all=True)
opts.merge(opts_def)

global_ns = self.shell.user_global_ns
local_ns = self.shell.user_ns

# Get the requested functions.
funcs = []
for name in opts.f:
try:
funcs.append(eval(name, global_ns, local_ns))
except Exception as e:
raise UsageError(f'Could not find module {name}.\n{e.__class__.__name__}: {e}')

profile = LineProfiler(*funcs)

# Get the modules, too
for modname in opts.m:
try:
mod = __import__(modname, fromlist=[''])
profile.add_module(mod)
except Exception as e:
raise UsageError(f'Could not find module {modname}.\n{e.__class__.__name__}: {e}')

if opts.u is not None:
try:
output_unit = float(opts.u[0])
except Exception:
raise TypeError('Timer unit setting must be a float.')
else:
output_unit = None

# Add the profiler to the builtins for @profile.
import builtins

if 'profile' in builtins.__dict__:
had_profile = True
old_profile = builtins.__dict__['profile']
else:
had_profile = False
old_profile = None
builtins.__dict__['profile'] = profile

try:
try:
profile.runctx(arg_str, global_ns, local_ns)
message = ''
except SystemExit:
message = """*** SystemExit exception caught in code being profiled."""
except KeyboardInterrupt:
message = ('*** KeyboardInterrupt exception caught in code being '
'profiled.')
finally:
if had_profile:
builtins.__dict__['profile'] = old_profile

# Trap text output.
stdout_trap = StringIO()
profile.print_stats(stdout_trap, output_unit=output_unit, stripzeros='s' in opts)
output = stdout_trap.getvalue()
output = output.rstrip()

page(output)
print(message, end='')

dump_file = opts.D[0]
if dump_file:
profile.dump_stats(dump_file)
print(f'\n*** Profile stats pickled to file {dump_file!r}. {message}')

text_file = opts.T[0]
if text_file:
pfile = open(text_file, 'w')
pfile.write(output)
pfile.close()
print(f'\n*** Profile printout saved to text file {text_file!r}. {message}')

return_value = None
if 'r' in opts:
return_value = profile

return return_value


def load_ipython_extension(ip):
""" API for IPython to recognize this module as an IPython extension.
"""
ip.register_magics(LineProfilerMagics)


def load_stats(filename):
""" Utility function to load a pickled LineStats object from a given
filename.
Expand Down
1 change: 1 addition & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
-r requirements/runtime.txt
-r requirements/ipython.txt
-r requirements/build.txt
-r requirements/tests.txt
2 changes: 2 additions & 0 deletions requirements/ipython.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
IPython >=0.13 ; python_version >= '3.7'
IPython >=0.13, <7.17.0 ; python_version <= '3.6'
2 changes: 0 additions & 2 deletions requirements/runtime.txt
Original file line number Diff line number Diff line change
@@ -1,2 +0,0 @@
IPython >=0.13 ; python_version >= '3.7'
IPython >=0.13, <7.17.0 ; python_version <= '3.6'
2 changes: 2 additions & 0 deletions requirements/tests.txt
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,5 @@ pytest >= 4.6.11
pytest-cov >= 2.10.1
coverage[toml] >= 5.3
ubelt >= 1.0.1
IPython >=0.13 ; python_version >= '3.7'
IPython >=0.13, <7.17.0 ; python_version <= '3.6'
1 change: 1 addition & 0 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -259,6 +259,7 @@ def native_mb_python_tag(plat_impl=None, version_info=None):
install_requires=parse_requirements('requirements/runtime.txt'),
extras_require={
'all': parse_requirements('requirements.txt'),
'ipython': parse_requirements('requirements/ipython.txt'),
'tests': parse_requirements('requirements/tests.txt'),
'build': parse_requirements('requirements/build.txt'),
},
Expand Down
Loading

0 comments on commit 8c7624c

Please sign in to comment.