From 84467296ad64f344113dc88eef2a20028768b21f Mon Sep 17 00:00:00 2001 From: M Bussonnier Date: Wed, 20 Nov 2024 13:20:28 +0100 Subject: [PATCH] Share some options between the CLI and magic. This refactor compute_render_options, to be shared between CLI and magic. Add a few of the CLI options to be available in the magic. The only thing I don't really like is that we redefine the help text options. --- docs/guide.md | 2 +- pyinstrument/__main__.py | 38 +++++++++++++--- pyinstrument/magic/magic.py | 86 ++++++++++++++++++++++++++++++++++--- 3 files changed, 112 insertions(+), 14 deletions(-) diff --git a/docs/guide.md b/docs/guide.md index 427d7c61..9be486cc 100644 --- a/docs/guide.md +++ b/docs/guide.md @@ -168,7 +168,7 @@ profile something. In this case, add `PYINSTRUMENT_PROFILE_DIR = 'profiles'` to your `settings.py`. Pyinstrument will profile every request and save the HTML output to the folder `profiles` in your working directory. -You can further customize the filename by adding `PYINSTRUMENT_FILENAME` to +You can further customize the filename by adding `PYINSTRUMENT_FILENAME` to `settings.py`, default values is `"{total_time:.3f}s {path} {timestamp:.0f}.{ext}"`. If you want to show the profiling page depending on the request you can define diff --git a/pyinstrument/__main__.py b/pyinstrument/__main__.py index dda0cab9..ae74047e 100644 --- a/pyinstrument/__main__.py +++ b/pyinstrument/__main__.py @@ -9,7 +9,7 @@ import shutil import sys import time -from typing import Any, List, TextIO, cast +from typing import Any, List, Optional, TextIO, Tuple, cast import pyinstrument from pyinstrument import Profiler, renderers @@ -412,9 +412,35 @@ def store_and_consume_remaining( def compute_render_options( options: CommandLineOptions, renderer_class: type[renderers.Renderer], output_file: TextIO ) -> dict[str, Any]: + """ + Given a list of CommandLineOoptionsCompute the rendering options compute the + rendering options for the given renderer. + + Raises if there is an error parsing the options. + + """ + + unicode_support: bool = file_supports_unicode(output_file) + + error, render_options = _compute_render_options(options, renderer_class, unicode_support) + if error is not None: + raise OptionsParseError(error) + assert render_options is not None + return render_options + + +def _compute_render_options( + options: CommandLineOptions, renderer_class: type[renderers.Renderer], unicode_support: bool +) -> Tuple[Optional[str], Optional[dict[str, Any]]]: + """ + Similar as compute_render_options, but return a tuple (error message, data) + if there is an error; this will let us reuse _compute_render_options in magics. + + output_file, has been replaced by unicode_support:bool + """ # parse show/hide options if options.hide_fnmatch is not None and options.hide_regex is not None: - raise OptionsParseError("You can‘t specify both --hide and --hide-regex") + return ("You can‘t specify both --hide and --hide-regex", None) hide_regex: str | None show_regex: str | None @@ -429,7 +455,7 @@ def compute_render_options( options.show_all, ] if show_options_used.count(True) > 1: - raise OptionsParseError("You can only specify one of --show, --show-regex and --show-all") + return ("You can only specify one of --show, --show-regex and --show-all", None) if options.show_fnmatch is not None: show_regex = fnmatch.translate(options.show_fnmatch) @@ -449,8 +475,8 @@ def compute_render_options( if issubclass(renderer_class, renderers.ConsoleRenderer): unicode_override = options.unicode is not None color_override = options.color is not None - unicode: Any = options.unicode if unicode_override else file_supports_unicode(output_file) - color: Any = options.color if color_override else file_supports_color(output_file) + unicode: Any = options.unicode if unicode_override else unicode_support + color: Any = options.color if color_override else unicode_support render_options.update({"unicode": unicode, "color": color}) @@ -478,7 +504,7 @@ def compute_render_options( keypath.set_value_at_keypath(render_options, key, parsed_value) - return render_options + return None, render_options class OptionsParseError(Exception): diff --git a/pyinstrument/magic/magic.py b/pyinstrument/magic/magic.py index 987339e1..e5ed5880 100644 --- a/pyinstrument/magic/magic.py +++ b/pyinstrument/magic/magic.py @@ -14,9 +14,12 @@ from IPython.display import IFrame, display from pyinstrument import Profiler, renderers +from pyinstrument.__main__ import _compute_render_options from pyinstrument.frame import Frame from pyinstrument.frame_ops import delete_frame_from_tree from pyinstrument.processors import ProcessorOptions +from pyinstrument.renderers.console import ConsoleRenderer +from pyinstrument.renderers.html import HTMLRenderer _active_profiler = None @@ -76,6 +79,43 @@ def recreate_transformer(self, target_description: str): ) @magic_arguments() + @argument( + "-p", + "--render-option", + dest="render_options", + action="append", + metavar="RENDER_OPTION", + type=str, + help=( + "options to pass to the renderer, in the format 'flag_name' or 'option_name=option_value'. " + "For example, to set the option 'time', pass '-p time=percent_of_total'. To pass multiple " + "options, use the -p option multiple times. You can set processor options using dot-syntax, " + "like '-p processor_options.filter_threshold=0'. option_value is parsed as a JSON value or " + "a string." + ), + ) + @argument( + "--show-regex", + dest="show_regex", + action="store", + metavar="REGEX", + help=( + "regex matching the file paths whose frames to always show. " + "Useful if --show doesn't give enough control." + ), + ) + @argument( + "--show", + dest="show_fnmatch", + action="store", + metavar="EXPR", + help=( + "glob-style pattern matching the file paths whose frames to " + "show, regardless of --hide or --hide-regex. For example, use " + "--show '*//*' to show frames within a library that " + "would otherwise be hidden." + ), + ) @argument( "--interval", type=float, @@ -110,6 +150,26 @@ def recreate_transformer(self, target_description: str): nargs="*", help="When used as a line magic, the code to profile", ) + @argument( + "--hide", + dest="hide_fnmatch", + action="store", + metavar="EXPR", + help=( + "glob-style pattern matching the file paths whose frames to hide. Defaults to " + "hiding non-application code" + ), + ) + @argument( + "--hide-regex", + dest="hide_regex", + action="store", + metavar="REGEX", + help=( + "regex matching the file paths whose frames to hide. Useful if --hide doesn't give " + "enough control." + ), + ) @no_var_expand @line_cell_magic def pyinstrument(self, line, cell=None): @@ -126,6 +186,12 @@ def pyinstrument(self, line, cell=None): """ global _active_profiler args = parse_argstring(self.pyinstrument, line) + + # 2024, always override this for now in IPython, + # we can make an option later if necessary + args.unicode = True + args.color = True + ip = get_ipython() if not ip: @@ -175,10 +241,19 @@ def pyinstrument(self, line, cell=None): ) return - html_renderer = renderers.HTMLRenderer( - show_all=args.show_all, - timeline=args.timeline, + html_error, html_config = _compute_render_options( + args, renderer_class=HTMLRenderer, unicode_support=True ) + if html_error is not None: + raise ValueError(html_error) + + text_error, text_config = _compute_render_options( + args, renderer_class=HTMLRenderer, unicode_support=True + ) + if text_error is not None: + raise ValueError(text_error) + + html_renderer = renderers.HTMLRenderer(show_all=args.show_all, timeline=args.timeline) html_renderer.preprocessors.append(strip_ipython_frames_processor) html_str = _active_profiler.output(html_renderer) as_iframe = IFrame( @@ -188,10 +263,7 @@ def pyinstrument(self, line, cell=None): extras=['style="resize: vertical"', f'srcdoc="{html.escape(html_str)}"'], ) - text_renderer = renderers.ConsoleRenderer( - timeline=args.timeline, - show_all=args.show_all, - ) + text_renderer = renderers.ConsoleRenderer(**text_config) text_renderer.processors.append(strip_ipython_frames_processor) as_text = _active_profiler.output(text_renderer)