From 4a5525531fec7bb2071c42a5f36c439b75631b0c Mon Sep 17 00:00:00 2001 From: Joseph Martinot-Lagarde Date: Wed, 9 Feb 2022 19:54:42 +0100 Subject: [PATCH 01/11] IPython is now optional --- README.rst | 4 + line_profiler/ipython_extension.py | 141 ++++++++++++++++++++++++++++ line_profiler/line_profiler.py | 146 +---------------------------- requirements.txt | 1 + requirements/ipython.txt | 2 + requirements/runtime.txt | 2 - setup.py | 1 + 7 files changed, 154 insertions(+), 143 deletions(-) create mode 100644 line_profiler/ipython_extension.py create mode 100644 requirements/ipython.txt diff --git a/README.rst b/README.rst index abd7e7c0..62d39daa 100644 --- a/README.rst +++ b/README.rst @@ -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 diff --git a/line_profiler/ipython_extension.py b/line_profiler/ipython_extension.py new file mode 100644 index 00000000..d6fb7f83 --- /dev/null +++ b/line_profiler/ipython_extension.py @@ -0,0 +1,141 @@ +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 + + +@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 + + 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 : 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 : Get all the functions/methods in a module + + One or more -f or -m options are required to get any useful results. + + -D : 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 : 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) diff --git a/line_profiler/line_profiler.py b/line_profiler/line_profiler.py index 4d037435..a0c6cad1 100755 --- a/line_profiler/line_profiler.py +++ b/line_profiler/line_profiler.py @@ -6,13 +6,13 @@ 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 ipython_extension import load_ipython_extension +except ImportError: + def load_ipython_extension(ip): + raise ImportError("Module IPython not found") try: from ._line_profiler import LineProfiler as CLineProfiler @@ -244,142 +244,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 - - 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 : 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 : Get all the functions/methods in a module - - One or more -f or -m options are required to get any useful results. - - -D : 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 : 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. diff --git a/requirements.txt b/requirements.txt index 17fa364f..db2de0ea 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,4 @@ -r requirements/runtime.txt +-r requirements/ipython.txt -r requirements/build.txt -r requirements/tests.txt diff --git a/requirements/ipython.txt b/requirements/ipython.txt new file mode 100644 index 00000000..bec65883 --- /dev/null +++ b/requirements/ipython.txt @@ -0,0 +1,2 @@ +IPython >=0.13 ; python_version >= '3.7' +IPython >=0.13, <7.17.0 ; python_version <= '3.6' diff --git a/requirements/runtime.txt b/requirements/runtime.txt index bec65883..e69de29b 100644 --- a/requirements/runtime.txt +++ b/requirements/runtime.txt @@ -1,2 +0,0 @@ -IPython >=0.13 ; python_version >= '3.7' -IPython >=0.13, <7.17.0 ; python_version <= '3.6' diff --git a/setup.py b/setup.py index 10300857..dae26cd5 100755 --- a/setup.py +++ b/setup.py @@ -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'), }, From cb13a7797675b3feda336419c76c3a94baa96c1c Mon Sep 17 00:00:00 2001 From: Joseph Martinot-Lagarde Date: Sun, 20 Feb 2022 18:48:06 +0100 Subject: [PATCH 02/11] Manage imports in __init__.py --- line_profiler/__init__.py | 15 +++++++++++---- line_profiler/line_profiler.py | 2 ++ 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/line_profiler/__init__.py b/line_profiler/__init__.py index 2136be93..16fdfd33 100644 --- a/line_profiler/__init__.py +++ b/line_profiler/__init__.py @@ -3,6 +3,7 @@ """ __submodules__ = [ 'line_profiler', + 'ipython_extension', ] __autogen__ = """ @@ -13,10 +14,16 @@ from .line_profiler import __version__ -from .line_profiler import (LineProfiler, LineProfilerMagics, - load_ipython_extension, load_stats, main, +from .line_profiler import (LineProfiler, load_stats, main, show_func, show_text,) -__all__ = ['LineProfiler', 'LineProfilerMagics', 'line_profiler', - 'load_ipython_extension', 'load_stats', 'main', 'show_func', +__all__ = ['LineProfiler', 'line_profiler', + 'load_stats', 'main', 'show_func', 'show_text', '__version__'] + +try: + from .ipython_extension import (LineProfilerMagics, load_ipython_extension,) + + __all__ += ['LineProfilerMagics', 'load_ipython_extension'] +except ImportError: + pass diff --git a/line_profiler/line_profiler.py b/line_profiler/line_profiler.py index a0c6cad1..247aa2f5 100755 --- a/line_profiler/line_profiler.py +++ b/line_profiler/line_profiler.py @@ -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. def is_ipython_kernel_cell(filename): """ Return True if a filename corresponds to a Jupyter Notebook cell """ From 6f92dca5184976419fe1b16b3cae7e1f4902ccb0 Mon Sep 17 00:00:00 2001 From: Joseph Martinot-Lagarde Date: Sun, 20 Feb 2022 18:52:27 +0100 Subject: [PATCH 03/11] Forgot local import --- line_profiler/ipython_extension.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/line_profiler/ipython_extension.py b/line_profiler/ipython_extension.py index d6fb7f83..cffc42f9 100644 --- a/line_profiler/ipython_extension.py +++ b/line_profiler/ipython_extension.py @@ -5,6 +5,8 @@ from IPython.utils.ipstruct import Struct from IPython.core.error import UsageError +from .line_profiler import LineProfiler + @magics_class class LineProfilerMagics(Magics): From 53c5c595a1bde6c28c885a7373d38dff943135df Mon Sep 17 00:00:00 2001 From: Joseph Martinot-Lagarde Date: Sun, 20 Feb 2022 19:06:06 +0100 Subject: [PATCH 04/11] Use load_ipython_extension from line_profiler to get a useful message --- line_profiler/__init__.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/line_profiler/__init__.py b/line_profiler/__init__.py index 16fdfd33..77668ac6 100644 --- a/line_profiler/__init__.py +++ b/line_profiler/__init__.py @@ -14,16 +14,17 @@ from .line_profiler import __version__ -from .line_profiler import (LineProfiler, load_stats, main, +from .line_profiler import (LineProfiler, + load_ipython_extension, load_stats, main, show_func, show_text,) __all__ = ['LineProfiler', 'line_profiler', - 'load_stats', 'main', 'show_func', + 'load_ipython_extension', 'load_stats', 'main', 'show_func', 'show_text', '__version__'] try: - from .ipython_extension import (LineProfilerMagics, load_ipython_extension,) + from .ipython_extension import (LineProfilerMagics, ,) - __all__ += ['LineProfilerMagics', 'load_ipython_extension'] + __all__ += ['LineProfilerMagics'] except ImportError: pass From d547295944de61b660cdde6f6b89cb8718d92d2e Mon Sep 17 00:00:00 2001 From: Joseph Martinot-Lagarde Date: Sun, 20 Feb 2022 19:07:17 +0100 Subject: [PATCH 05/11] Typo --- line_profiler/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/line_profiler/__init__.py b/line_profiler/__init__.py index 77668ac6..eff00928 100644 --- a/line_profiler/__init__.py +++ b/line_profiler/__init__.py @@ -23,7 +23,7 @@ 'show_text', '__version__'] try: - from .ipython_extension import (LineProfilerMagics, ,) + from .ipython_extension import LineProfilerMagics __all__ += ['LineProfilerMagics'] except ImportError: From dd4c4254ad2664196ee4c17c4d5fccdd48e03830 Mon Sep 17 00:00:00 2001 From: Joseph Martinot-Lagarde Date: Mon, 21 Feb 2022 01:09:57 +0100 Subject: [PATCH 06/11] Rework IPython plugin layout --- line_profiler/__init__.py | 16 +- line_profiler/ipython_extension.py | 241 +++++++++++++++-------------- line_profiler/line_profiler.py | 6 - 3 files changed, 129 insertions(+), 134 deletions(-) diff --git a/line_profiler/__init__.py b/line_profiler/__init__.py index eff00928..74c80ac6 100644 --- a/line_profiler/__init__.py +++ b/line_profiler/__init__.py @@ -14,17 +14,11 @@ from .line_profiler import __version__ -from .line_profiler import (LineProfiler, - load_ipython_extension, load_stats, main, +from .line_profiler import (LineProfiler, load_stats, main, show_func, show_text,) -__all__ = ['LineProfiler', 'line_profiler', - 'load_ipython_extension', 'load_stats', 'main', 'show_func', - 'show_text', '__version__'] - -try: - from .ipython_extension import LineProfilerMagics +from .ipython_extension import load_ipython_extension, LineProfilerMagics - __all__ += ['LineProfilerMagics'] -except ImportError: - pass +__all__ = ['LineProfiler', 'LineProfilerMagics', 'line_profiler', + 'load_ipython_extension', 'load_stats', 'main', 'show_func', + 'show_text', '__version__'] \ No newline at end of file diff --git a/line_profiler/ipython_extension.py b/line_profiler/ipython_extension.py index cffc42f9..49fbadbf 100644 --- a/line_profiler/ipython_extension.py +++ b/line_profiler/ipython_extension.py @@ -1,143 +1,150 @@ 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 +LineProfilerMagics = None -@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. +def load_ipython_extension(ip): + """ API for IPython to recognize this module as an IPython extension. + """ - Usage: - %lprun -f func1 -f func2 + # Import IPython inside the function to load IPython only if necessary + 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 - 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. + # Lazy import + global LineProfilerMagics - Options: + @magics_class + class LineProfilerMagics(Magics): - -f : 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. + @line_magic + def lprun(self, parameter_s=''): + """ Execute a statement under the line-by-line profiler from the + line_profiler module. - -m : Get all the functions/methods in a module + Usage: + %lprun -f func1 -f func2 - One or more -f or -m options are required to get any useful results. + 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. - -D : 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. + Options: - -T : dump the text-formatted statistics with the code side-by-side - out to a text file. + -f : 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. - -r: return the LineProfiler object after it has completed profiling. + -m : Get all the functions/methods in a module - -s: strip out all entries from the print-out that have zeros. + One or more -f or -m options are required to get any useful results. - -u: specify time unit for the print-out in seconds. - """ + -D : 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. - # 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) + -T : dump the text-formatted statistics with the code side-by-side + out to a text file. - global_ns = self.shell.user_global_ns - local_ns = self.shell.user_ns + -r: return the LineProfiler object after it has completed profiling. - # 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}') + -s: strip out all entries from the print-out that have zeros. - profile = LineProfiler(*funcs) + -u: specify time unit for the print-out in seconds. + """ - # 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}') + # 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) - 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 + 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}') -def load_ipython_extension(ip): - """ API for IPython to recognize this module as an IPython extension. - """ + 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 + + # Register the extension in IPython ip.register_magics(LineProfilerMagics) diff --git a/line_profiler/line_profiler.py b/line_profiler/line_profiler.py index 247aa2f5..0af1d92e 100755 --- a/line_profiler/line_profiler.py +++ b/line_profiler/line_profiler.py @@ -8,12 +8,6 @@ import sys from argparse import ArgumentError, ArgumentParser -try: - from ipython_extension import load_ipython_extension -except ImportError: - def load_ipython_extension(ip): - raise ImportError("Module IPython not found") - try: from ._line_profiler import LineProfiler as CLineProfiler except ImportError as ex: From adfc2cbbeb1058ae67e862c256df3b523a5a4ecf Mon Sep 17 00:00:00 2001 From: Joseph Martinot-Lagarde Date: Mon, 21 Feb 2022 01:11:46 +0100 Subject: [PATCH 07/11] Update CHANGELOG --- CHANGELOG.rst | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 808141e1..062dda28 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -4,11 +4,12 @@ Changes 3.5.0 ~~~~~ * FIX: Fixes max of an empty sequence error #118 +* Make IPython optional 3.4.0 ~~~~~ * Drop support for Python <= 3.5.x -* FIX: #104 issue with new IPython kernels +* FIX: #104 issue with new IPython kernels 3.3.1 ~~~~~ @@ -22,7 +23,7 @@ Changes 3.2.6 ~~~~~ * FIX: Update MANIFEST.in to package pyproj.toml and missing pyx file -* CHANGE: Removed version experimental augmentation. +* CHANGE: Removed version experimental augmentation. 3.2.5 ~~~~~ @@ -44,7 +45,7 @@ Changes 3.2.0 ~~~~~ -* Dropped 2.7 support, manylinux docker images no longer support 2.7 +* Dropped 2.7 support, manylinux docker images no longer support 2.7 * ENH: Add command line option to specify time unit and skip displaying functions which have not been profiled. * ENH: Unified versions of line_profiler and kernprof: kernprof version is now @@ -114,4 +115,3 @@ Changes ~~~~~ * Initial release. - From 5de7781cc9248fa00b6b922cb8c10f6767cd3109 Mon Sep 17 00:00:00 2001 From: Joseph Martinot-Lagarde Date: Mon, 21 Feb 2022 21:45:58 +0100 Subject: [PATCH 08/11] Load IPython only when needed --- line_profiler/__init__.py | 9 +- line_profiler/ipython_extension.py | 249 ++++++++++++++--------------- line_profiler/line_profiler.py | 7 + 3 files changed, 132 insertions(+), 133 deletions(-) diff --git a/line_profiler/__init__.py b/line_profiler/__init__.py index 74c80ac6..93d77008 100644 --- a/line_profiler/__init__.py +++ b/line_profiler/__init__.py @@ -14,11 +14,10 @@ from .line_profiler import __version__ -from .line_profiler import (LineProfiler, load_stats, main, +from .line_profiler import (LineProfiler, + load_ipython_extension, load_stats, main, show_func, show_text,) -from .ipython_extension import load_ipython_extension, LineProfilerMagics - -__all__ = ['LineProfiler', 'LineProfilerMagics', 'line_profiler', +__all__ = ['LineProfiler', 'line_profiler', 'load_ipython_extension', 'load_stats', 'main', 'show_func', - 'show_text', '__version__'] \ No newline at end of file + 'show_text', '__version__'] diff --git a/line_profiler/ipython_extension.py b/line_profiler/ipython_extension.py index 49fbadbf..87c662ac 100644 --- a/line_profiler/ipython_extension.py +++ b/line_profiler/ipython_extension.py @@ -1,150 +1,143 @@ from io import StringIO -from .line_profiler import LineProfiler - -LineProfilerMagics = None - - -def load_ipython_extension(ip): - """ API for IPython to recognize this module as an IPython extension. - """ - - # Import IPython inside the function to load IPython only if necessary - 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 - - # Lazy import - global LineProfilerMagics - - @magics_class - class LineProfilerMagics(Magics): +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 - @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 - - 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. +from .line_profiler import LineProfiler - Options: - -f : 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. +@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. - -m : Get all the functions/methods in a module + Usage: + %lprun -f func1 -f func2 - One or more -f or -m options are required to get any useful results. + 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. - -D : 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. + Options: - -T : dump the text-formatted statistics with the code side-by-side - out to a text file. + -f : 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. - -r: return the LineProfiler object after it has completed profiling. + -m : Get all the functions/methods in a module - -s: strip out all entries from the print-out that have zeros. + One or more -f or -m options are required to get any useful results. - -u: specify time unit for the print-out in seconds. - """ + -D : 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. - # 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) + -T : dump the text-formatted statistics with the code side-by-side + out to a text file. - global_ns = self.shell.user_global_ns - local_ns = self.shell.user_ns + -r: return the LineProfiler object after it has completed profiling. - # 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}') + -s: strip out all entries from the print-out that have zeros. - profile = LineProfiler(*funcs) + -u: specify time unit for the print-out in seconds. + """ - # 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}') + # 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) - 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 + global_ns = self.shell.user_global_ns + local_ns = self.shell.user_ns - # Add the profiler to the builtins for @profile. - import builtins + # 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}" + ) - if 'profile' in builtins.__dict__: - had_profile = True - old_profile = builtins.__dict__['profile'] - else: - had_profile = False - old_profile = None - builtins.__dict__['profile'] = profile + 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: - 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 - - # Register the extension in IPython - ip.register_magics(LineProfilerMagics) + 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 diff --git a/line_profiler/line_profiler.py b/line_profiler/line_profiler.py index 595037f9..7d9312a9 100755 --- a/line_profiler/line_profiler.py +++ b/line_profiler/line_profiler.py @@ -19,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 From 56e5a74d29421eb920f68a0547f2a4d474582801 Mon Sep 17 00:00:00 2001 From: Joseph Martinot-Lagarde Date: Wed, 23 Feb 2022 23:28:19 +0100 Subject: [PATCH 09/11] Add basic ipython test --- requirements/tests.txt | 2 ++ tests/test_ipython.py | 12 ++++++++++++ 2 files changed, 14 insertions(+) create mode 100644 tests/test_ipython.py diff --git a/requirements/tests.txt b/requirements/tests.txt index f5cec548..b960c337 100644 --- a/requirements/tests.txt +++ b/requirements/tests.txt @@ -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' diff --git a/tests/test_ipython.py b/tests/test_ipython.py new file mode 100644 index 00000000..f91695ca --- /dev/null +++ b/tests/test_ipython.py @@ -0,0 +1,12 @@ +import unittest +import io + +from IPython.testing.globalipapp import get_ipython + +class TestIPython(unittest.TestCase): + def test_init(self): + ip = get_ipython() + ip.magic('load_ext line_profiler') + ip.run_cell(raw_cell="def func():\n return 2**20") + ip.run_line_magic('lprun', '-f func func()') + # TODO: Check output From 583cf272b04e581c7975db1d0fa6ad52e356adc0 Mon Sep 17 00:00:00 2001 From: Joseph Martinot-Lagarde Date: Sun, 6 Mar 2022 00:09:31 +0100 Subject: [PATCH 10/11] Fix deprecation warning --- tests/test_ipython.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_ipython.py b/tests/test_ipython.py index f91695ca..800bc563 100644 --- a/tests/test_ipython.py +++ b/tests/test_ipython.py @@ -6,7 +6,7 @@ class TestIPython(unittest.TestCase): def test_init(self): ip = get_ipython() - ip.magic('load_ext line_profiler') - ip.run_cell(raw_cell="def func():\n return 2**20") + ip.run_line_magic('load_ext', 'line_profiler') + ip.run_cell(raw_cell='def func():\n return 2**20') ip.run_line_magic('lprun', '-f func func()') # TODO: Check output From 007efb75bc0650c28080bdb39b5df45f8b4d3b9d Mon Sep 17 00:00:00 2001 From: Joseph Martinot-Lagarde Date: Sun, 6 Mar 2022 00:28:24 +0100 Subject: [PATCH 11/11] Check output --- tests/test_ipython.py | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/tests/test_ipython.py b/tests/test_ipython.py index 800bc563..76eb0f83 100644 --- a/tests/test_ipython.py +++ b/tests/test_ipython.py @@ -8,5 +8,14 @@ def test_init(self): ip = get_ipython() ip.run_line_magic('load_ext', 'line_profiler') ip.run_cell(raw_cell='def func():\n return 2**20') - ip.run_line_magic('lprun', '-f func func()') - # TODO: Check output + lprof = ip.run_line_magic('lprun', '-r -f func func()') + + timings = lprof.get_stats().timings + self.assertEqual(len(timings), 1) # 1 function + + func_data, lines_data = next(iter(timings.items())) + self.assertEqual(func_data[1], 1) # lineno of the function + self.assertEqual(func_data[2], "func") # function name + self.assertEqual(len(lines_data), 1) # 1 line of code + self.assertEqual(lines_data[0][0], 2) # lineno + self.assertEqual(lines_data[0][1], 1) # hits