From 266388ea51a3f2fc8ec9eea5180820f57f94d999 Mon Sep 17 00:00:00 2001 From: M Bussonnier Date: Wed, 20 Nov 2024 14:05:28 +0100 Subject: [PATCH] Share some options between the CLI and magic. This refactor compute_render_options, to be shared between CLI and magic, it changes the last parameter from a file-like output to a boolean to control wether or not the output will support color and unicode. The only thing I don't really like is that we redefine the help text options. Co-authored-by: Joe Rickerby --- pyinstrument/__main__.py | 37 +++++++++++++---- pyinstrument/magic/magic.py | 82 +++++++++++++++++++++++++++++++++---- 2 files changed, 104 insertions(+), 15 deletions(-) diff --git a/pyinstrument/__main__.py b/pyinstrument/__main__.py index dda0cab9..d204aea5 100644 --- a/pyinstrument/__main__.py +++ b/pyinstrument/__main__.py @@ -409,9 +409,31 @@ def store_and_consume_remaining( raise inner_exception +class OptionsParseError(Exception): + pass + + def compute_render_options( - options: CommandLineOptions, renderer_class: type[renderers.Renderer], output_file: TextIO + options: CommandLineOptions, + renderer_class: type[renderers.Renderer], + unicode_support: bool, + color_support: bool, ) -> dict[str, Any]: + """ + Given a list of `CommandLineOptions`, compute the + rendering options for the given renderer. + + Raises an `OptionsParseError` if there is an error parsing the options. + + unicode_support: + indicate whether the expected output supports unicode + color_support: + indicate whether the expected output supports color + + Both of these will be used to determine the default of outputting unicode + or color, but can be overridden with `options.color` and `option.unicode`. + """ + # 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") @@ -449,8 +471,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 color_support render_options.update({"unicode": unicode, "color": color}) @@ -481,15 +503,14 @@ def compute_render_options( return render_options -class OptionsParseError(Exception): - pass - - def create_renderer( renderer_class: type[renderers.Renderer], options: CommandLineOptions, output_file: TextIO ) -> renderers.Renderer: render_options = compute_render_options( - options, renderer_class=renderer_class, output_file=output_file + options, + renderer_class=renderer_class, + unicode_support=file_supports_unicode(output_file), + color_support=file_supports_color(output_file), ) try: diff --git a/pyinstrument/magic/magic.py b/pyinstrument/magic/magic.py index 987339e1..dc18306d 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,15 @@ def pyinstrument(self, line, cell=None): ) return - html_renderer = renderers.HTMLRenderer( - show_all=args.show_all, - timeline=args.timeline, + html_config = compute_render_options( + args, renderer_class=HTMLRenderer, unicode_support=True, color_support=True ) + + text_config = compute_render_options( + args, renderer_class=HTMLRenderer, unicode_support=True, color_support=True + ) + + 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 +259,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)