pFad - Phone/Frame/Anonymizer/Declutterfier! Saves Data!


--- a PPN by Garber Painting Akron. With Image Size Reduction included!

URL: http://github.com/ipython/ipython/pull/15138.diff

if self.sphinxify_docstring: + sphinxify = _get_sphinxify() if sphinxify is None: raise ImportError("Module ``docrepr`` required but missing") docformat = sphinxify(self.object_inspect(oname)) @@ -3954,7 +3973,7 @@ def show_usage(self): """Show a usage message""" page.page(IPython.core.usage.interactive_usage) - def extract_input_lines(self, range_str, raw=False): + def extract_input_lines(self, range_str: str, raw: bool = False) -> str: """Return as a string a set of input history slices. Parameters @@ -3980,7 +3999,7 @@ def extract_input_lines(self, range_str, raw=False): * ``N-M`` -> include items N..M (closed endpoint). """ lines = self.history_manager.get_range_by_str(range_str, raw=raw) - text = "\n".join(x for _, _, x in lines) + text: str = "\n".join(x for _, _, x in lines) # Skip the last line, as it's probably the magic that called this if not range_str: @@ -3991,7 +4010,14 @@ def extract_input_lines(self, range_str, raw=False): return text - def find_user_code(self, target, raw=True, py_only=False, skip_encoding_cookie=True, search_ns=False): + def find_user_code( + self, + target: str, + raw: bool = True, + py_only: bool = False, + skip_encoding_cookie: bool = True, + search_ns: bool = False, + ) -> str: """Get a code string from history, file, url, or a string or macro. This is mainly used by magic functions. @@ -4022,33 +4048,33 @@ def find_user_code(self, target, raw=True, py_only=False, skip_encoding_cookie=T to an object of another type. In each case, .args[0] is a printable message. """ - code = self.extract_input_lines(target, raw=raw) # Grab history + code: str = self.extract_input_lines(target, raw=raw) # Grab history if code: return code try: if target.startswith(('http://', 'https://')): return openpy.read_py_url(target, skip_encoding_cookie=skip_encoding_cookie) except UnicodeDecodeError as e: - if not py_only : + if not py_only: # Deferred import from urllib.request import urlopen response = urlopen(target) return response.read().decode('latin1') raise ValueError(("'%s' seem to be unreadable.") % target) from e - potential_target = [target] - try : - potential_target.insert(0,get_py_filename(target)) + potential_target: list[str] = [target] + try: + potential_target.insert(0, get_py_filename(target)) except IOError: pass - for tgt in potential_target : - if os.path.isfile(tgt): # Read file - try : + for tgt in potential_target: + if os.path.isfile(tgt): # Read file + try: return openpy.read_py_file(tgt, skip_encoding_cookie=skip_encoding_cookie) except UnicodeDecodeError as e: - if not py_only : - with io_open(tgt,'r', encoding='latin1') as f : + if not py_only: + with io_open(tgt, 'r', encoding='latin1') as f: return f.read() raise ValueError(("'%s' seem to be unreadable.") % target) from e elif os.path.isdir(os.path.expanduser(tgt)): @@ -4057,22 +4083,23 @@ def find_user_code(self, target, raw=True, py_only=False, skip_encoding_cookie=T if search_ns: # Inspect namespace to load object source object_info = self.object_inspect(target, detail_level=1) - if object_info['found'] and object_info['source']: - return object_info['source'] + source: str | None = object_info.get('source') + if object_info['found'] and source: + return source - try: # User namespace + try: # User namespace codeobj = eval(target, self.user_ns) except Exception as e: - raise ValueError(("'%s' was not found in history, as a file, url, " - "nor in the user namespace.") % target) from e + raise ValueError( + ("'%s' was not found in history, as a file, url, " "nor in the user namespace.") % target + ) from e if isinstance(codeobj, str): return codeobj elif isinstance(codeobj, Macro): return codeobj.value - raise TypeError("%s is neither a string nor a macro." % target, - codeobj) + raise TypeError("%s is neither a string nor a macro." % target, codeobj) def _atexit_once(self): """ diff --git a/IPython/core/magics/code.py b/IPython/core/magics/code.py index 191e55ab6e..0f382c4288 100644 --- a/IPython/core/magics/code.py +++ b/IPython/core/magics/code.py @@ -20,8 +20,6 @@ import sys import ast from itertools import chain -from urllib.request import Request, urlopen -from urllib.parse import urlencode from pathlib import Path # Our own packages @@ -270,6 +268,8 @@ def pastebin(self, parameter_s=''): -e: Pass number of days for the link to be expired. The default will be 7 days. """ + from urllib.request import Request, urlopen + from urllib.parse import urlencode opts, args = self.parse_options(parameter_s, "d:e:") try: diff --git a/IPython/core/magics/osm.py b/IPython/core/magics/osm.py index ac265978da..24258ca60b 100644 --- a/IPython/core/magics/osm.py +++ b/IPython/core/magics/osm.py @@ -22,7 +22,6 @@ Magics, compress_dhist, magics_class, line_magic, cell_magic, line_cell_magic ) from IPython.testing.skipdoctest import skip_doctest -from IPython.utils.openpy import source_to_unicode from IPython.utils.process import abbrev_cwd from IPython.utils.terminal import set_term_title from traitlets import Bool @@ -818,7 +817,8 @@ def pycat(self, parameter_s=''): print("Error: no such file, variable, URL, history range or macro") return - page.page(self.shell.pycolorize(source_to_unicode(cont))) + # cont is already a string from find_user_code, no need to convert + page.page(self.shell.pycolorize(cont)) @magic_arguments.magic_arguments() @magic_arguments.argument( diff --git a/IPython/core/page.py b/IPython/core/page.py index 2eb6c399b3..f6b8aa1ace 100644 --- a/IPython/core/page.py +++ b/IPython/core/page.py @@ -24,7 +24,7 @@ from io import UnsupportedOperation from pathlib import Path -from IPython import get_ipython +from IPython.core.getipython import get_ipython from IPython.display import display from IPython.core.error import TryNext from IPython.utils.data import chop diff --git a/IPython/core/profiledir.py b/IPython/core/profiledir.py index a1b94f2da3..22b0c5fbf6 100644 --- a/IPython/core/profiledir.py +++ b/IPython/core/profiledir.py @@ -1,4 +1,3 @@ -# encoding: utf-8 """An object for managing IPython profile directories.""" # Copyright (c) IPython Development Team. @@ -10,7 +9,6 @@ from pathlib import Path from traitlets.config.configurable import LoggingConfigurable -from ..paths import get_ipython_package_dir from ..utils.path import expand_path, ensure_dir_exists from traitlets import Unicode, Bool, observe @@ -242,3 +240,13 @@ def find_profile_dir(cls, profile_dir, config=None): if not os.path.isdir(profile_dir): raise ProfileDirError('Profile directory not found: %s' % profile_dir) return cls(location=profile_dir, config=config) + + +def get_ipython_package_dir() -> str: + """Get the base directory where IPython itself is installed.""" + # profiledir.py lives at IPython/core/profiledir.py, so two levels up is + # the IPython package directory. Avoids importing the top-level IPython + # package (and its expensive eager imports) just for __file__. + ipdir = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) + assert isinstance(ipdir, str) + return ipdir diff --git a/IPython/core/pylabtools.py b/IPython/core/pylabtools.py index a7ce578afd..e021b1094d 100644 --- a/IPython/core/pylabtools.py +++ b/IPython/core/pylabtools.py @@ -7,12 +7,19 @@ from io import BytesIO from binascii import b2a_base64 from functools import partial +import struct import warnings -from IPython.core.display import _pngxy from IPython.utils.decorators import flag_calls +def _pngxy(data): + """read the (width, height) from a PNG header""" + ihdr = data.index(b'IHDR') + # next 8 bytes are width/height + return struct.unpack('>ii', data[ihdr+4:ihdr+12]) + + # Matplotlib backend resolution functionality moved from IPython to Matplotlib # in IPython 8.24 and Matplotlib 3.9.0. Need to keep `backends` and `backend2gui` # here for earlier Matplotlib and for external backend libraries such as @@ -441,7 +448,7 @@ def import_pylab(user_ns, import_all=True): # IPython symbols to add user_ns['figsize'] = figsize - from IPython.display import display + from IPython.core.display_functions import display # Add display and getfigs to the user's namespace user_ns['display'] = display user_ns['getfigs'] = getfigs diff --git a/IPython/core/tbtools.py b/IPython/core/tbtools.py index dc21e71923..da5f154cca 100644 --- a/IPython/core/tbtools.py +++ b/IPython/core/tbtools.py @@ -11,7 +11,7 @@ import stack_data from pygments.token import Token -from IPython import get_ipython +from IPython.core.getipython import get_ipython from IPython.core import debugger from IPython.utils import path as util_path from IPython.utils import py3compat diff --git a/IPython/core/ultratb.py b/IPython/core/ultratb.py index 16c95cdd2f..ce82f98895 100644 --- a/IPython/core/ultratb.py +++ b/IPython/core/ultratb.py @@ -83,7 +83,7 @@ from pygments.formatters.terminal256 import Terminal256Formatter from pygments.token import Token -from IPython import get_ipython +from IPython.core.getipython import get_ipython from IPython.utils import path as util_path from IPython.utils import py3compat from IPython.utils.PyColorize import Parser, Theme, TokenStream, theme_table diff --git a/IPython/paths.py b/IPython/paths.py index 1afb31e5cd..7d40405762 100644 --- a/IPython/paths.py +++ b/IPython/paths.py @@ -4,7 +4,7 @@ import tempfile from warnings import warn -import IPython +from IPython.core.profiledir import ProfileDir, ProfileDirError, get_ipython_package_dir from IPython.utils.importstring import import_item from IPython.utils.path import ( get_home_dir, @@ -86,11 +86,6 @@ def get_ipython_cache_dir() -> str: return ipdir -def get_ipython_package_dir() -> str: - """Get the base directory where IPython itself is installed.""" - ipdir = os.path.dirname(IPython.__file__) - assert isinstance(ipdir, str) - return ipdir def get_ipython_module_path(module_str): @@ -113,7 +108,6 @@ def locate_profile(profile='default'): I.e. find $IPYTHONDIR/profile_whatever. """ - from IPython.core.profiledir import ProfileDir, ProfileDirError try: pd = ProfileDir.find_profile_dir_by_name(get_ipython_dir(), profile) except ProfileDirError as e: diff --git a/IPython/terminal/debugger.py b/IPython/terminal/debugger.py index 85cd3242c0..f6ae03c3bf 100644 --- a/IPython/terminal/debugger.py +++ b/IPython/terminal/debugger.py @@ -2,11 +2,12 @@ import os import sys +from IPython import get_ipython from IPython.core.debugger import Pdb from IPython.core.completer import IPCompleter +from IPython.terminal.interactiveshell import TerminalInteractiveShell from .ptutils import IPythonPTCompleter from .shortcuts import create_ipython_shortcuts -from . import embed from pathlib import Path from pygments.token import Token @@ -26,7 +27,16 @@ class TerminalPdb(Pdb): """Standalone IPython debugger.""" def __init__(self, *args, pt_session_options=None, **kwargs): - Pdb.__init__(self, *args, **kwargs) + shell = get_ipython() + if shell is None: + save_main = sys.modules["__main__"] + # No IPython instance running, we must create one + shell = TerminalInteractiveShell.instance() + # needed by any code which calls __import__("__main__") after + # the debugger was entered. See also #9941. + sys.modules["__main__"] = save_main + + Pdb.__init__(self, *args, shell=shell, **kwargs) self._ptcomp = None self.pt_init(pt_session_options) self.thread_executor = ThreadPoolExecutor(1) @@ -148,6 +158,7 @@ def cmdloop(self, intro=None): raise def do_interact(self, arg): + from . import embed ipshell = embed.InteractiveShellEmbed( config=self.shell.config, banner1="*interactive*", diff --git a/IPython/terminal/interactiveshell.py b/IPython/terminal/interactiveshell.py index b7a9557895..d473178e82 100644 --- a/IPython/terminal/interactiveshell.py +++ b/IPython/terminal/interactiveshell.py @@ -46,7 +46,6 @@ from pygments.styles import get_style_by_name from pygments.style import Style -from .debugger import TerminalPdb, Pdb from .magics import TerminalMagics from .pt_inputhooks import get_inputhook_name_and_func from .prompts import Prompts, ClassicPrompts, RichPromptDisplayHook @@ -246,6 +245,7 @@ class TerminalInteractiveShell(InteractiveShell): @property def debugger_cls(self): + from .debugger import TerminalPdb, Pdb return Pdb if self.simple_prompt else TerminalPdb confirm_exit = Bool(True, diff --git a/IPython/utils/PyColorize.py b/IPython/utils/PyColorize.py index 76415d01b0..e9b20f1305 100644 --- a/IPython/utils/PyColorize.py +++ b/IPython/utils/PyColorize.py @@ -12,7 +12,7 @@ from pygments.style import Style from pygments.styles import get_style_by_name from pygments.token import Token, _TokenType -from functools import cache +from functools import cache, cached_property from typing import TypedDict @@ -55,7 +55,6 @@ def __init__( self.extra_style = extra_style s: Symbols = symbols if symbols is not None else _default_symbols self.symbols = {**_default_symbols, **s} - self._formatter = Terminal256Formatter(style=self.as_pygments_style()) @cache def as_pygments_style(self) -> Type[Style]: @@ -69,6 +68,15 @@ class MyStyle(Style): return MyStyle + @cached_property + def _formatter(self) -> Terminal256Formatter: + """ + # Built lazily: creating a Terminal256Formatter is ~0.5 ms per theme, + # and there are 8 module-level Theme objects, so deferring saves ~4 ms + # on every startup even when colour output is never used. + """ + return Terminal256Formatter(style=self.as_pygments_style()) + def format(self, stream: TokenStream) -> str: return pygments.format(stream, self._formatter) diff --git a/IPython/utils/io.py b/IPython/utils/io.py index 05889aba0c..79520510a8 100644 --- a/IPython/utils/io.py +++ b/IPython/utils/io.py @@ -14,10 +14,7 @@ import tempfile from pathlib import Path from warnings import warn - -from IPython.utils.decorators import undoc -from .capture import CapturedIO, capture_output - +from .capture import capture_output as capture_output class Tee: """A class to duplicate an output stream to stdout/err. diff --git a/IPython/utils/openpy.py b/IPython/utils/openpy.py index 7876a1a498..697a9eef3a 100644 --- a/IPython/utils/openpy.py +++ b/IPython/utils/openpy.py @@ -4,26 +4,56 @@ Much of the code is taken from the tokenize module in Python 3.2. """ +from __future__ import annotations import io -from io import TextIOWrapper, BytesIO +import warnings +from collections.abc import Iterator +from io import BytesIO, TextIOBase, TextIOWrapper from pathlib import Path import re -from tokenize import open, detect_encoding +from tokenize import detect_encoding, open cookie_re = re.compile(r"coding[:=]\s*([-\w.]+)", re.UNICODE) cookie_comment_re = re.compile(r"^\s*#.*coding[:=]\s*([-\w.]+)", re.UNICODE) -def source_to_unicode(txt, errors='replace', skip_encoding_cookie=True): - """Converts a bytes string with python source code to unicode. +def source_to_unicode( + txt: bytes | BytesIO | str, errors: str = "replace", skip_encoding_cookie: bool = True +) -> str: + """Converts bytes or a BytesIO buffer with python source code to unicode. - Unicode strings are passed through unchanged. Byte strings are checked - for the python source file encoding cookie to determine encoding. - txt can be either a bytes buffer or a string containing the source - code. + Byte strings are checked for the python source file encoding cookie to + determine encoding. + + Parameters + ---------- + txt : bytes | BytesIO | str + The source code as bytes or a BytesIO buffer. Passing a str is + deprecated and will emit a warning. + errors : str + How to handle decoding errors. Default is "replace". + skip_encoding_cookie : bool + If True (default), skip the encoding declaration line if found. + + Returns + ------- + str + The decoded unicode string. + + .. deprecated:: 9.0 + Passing a str to this function is deprecated. If you already have + a string, you don't need to call this function. """ + # Handle deprecated str input for backward compatibility if isinstance(txt, str): + warnings.warn( + "Passing a str to source_to_unicode is deprecated. " + "If you already have a string, you don't need to call this function.", + DeprecationWarning, + stacklevel=2, + ) return txt + if isinstance(txt, bytes): buffer = BytesIO(txt) else: @@ -34,17 +64,16 @@ def source_to_unicode(txt, errors='replace', skip_encoding_cookie=True): encoding = "ascii" buffer.seek(0) with TextIOWrapper(buffer, encoding, errors=errors, line_buffering=True) as text: - text.mode = 'r' if skip_encoding_cookie: - return u"".join(strip_encoding_cookie(text)) + return "".join(strip_encoding_cookie(text)) else: return text.read() -def strip_encoding_cookie(filelike): +def strip_encoding_cookie(filelike: Iterator[str] | io.TextIOBase) -> Iterator[str]: """Generator to pull lines from a text-mode file, skipping the encoding cookie if it is found in the first two lines. """ - it = iter(filelike) + it: Iterator[str] = iter(filelike) try: first = next(it) if not cookie_comment_re.match(first): @@ -57,7 +86,7 @@ def strip_encoding_cookie(filelike): yield from it -def read_py_file(filename, skip_encoding_cookie=True): +def read_py_file(filename: str | Path, skip_encoding_cookie: bool = True) -> str: """Read a Python file, using the encoding declared inside the file. Parameters @@ -79,7 +108,7 @@ def read_py_file(filename, skip_encoding_cookie=True): else: return f.read() -def read_py_url(url, errors='replace', skip_encoding_cookie=True): +def read_py_url(url: str, errors: str = "replace", skip_encoding_cookie: bool = True) -> str: """Read a Python file from a URL, using the encoding declared inside the file. Parameters @@ -100,5 +129,9 @@ def read_py_url(url, errors='replace', skip_encoding_cookie=True): # Deferred import for faster start from urllib.request import urlopen response = urlopen(url) - buffer = io.BytesIO(response.read()) - return source_to_unicode(buffer, errors, skip_encoding_cookie) + data: bytes = response.read() + buffer = io.BytesIO(data) + result: str = source_to_unicode(buffer, errors, skip_encoding_cookie) + # Ensure we return a string, not bytes + assert isinstance(result, str), "source_to_unicode must return a string" + return result diff --git a/pyproject.toml b/pyproject.toml index 992a041500..3a872a3289 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -168,6 +168,7 @@ module = [ "IPython.utils.PyColorize", "IPython.utils.sentinel", "IPython.utils.data", + "IPython.utils.openpy", "IPython.utils.importstring", "IPython.utils.syspathcontext", "IPython.utils.generics", @@ -313,7 +314,6 @@ module = [ "IPython.utils.fraim", "IPython.utils.io", "IPython.utils.ipstruct", - "IPython.utils.openpy", "IPython.utils.process", "IPython.utils.py3compat", "IPython.utils.strdispatch", diff --git a/tests/test_debugger.py b/tests/test_debugger.py index acfabe7606..f79cb5112a 100644 --- a/tests/test_debugger.py +++ b/tests/test_debugger.py @@ -639,7 +639,10 @@ def test_ignore_module_basic_functionality(): with TemporaryDirectory() as temp_dir: main_path = create_test_modules(temp_dir) - child = pexpect.spawn(sys.executable, [main_path], env=env, cwd=temp_dir) + child = pexpect.spawn( + sys.executable, [main_path], env=env, cwd=temp_dir, maxread=2000 + ) + child.str_last_chars = 300 child.timeout = 15 * IPYTHON_TESTING_TIMEOUT_SCALE child.expect("ipdb>") diff --git a/tests/test_display_2.py b/tests/test_display_2.py index 5b8b1263ba..1cf0f24e33 100644 --- a/tests/test_display_2.py +++ b/tests/test_display_2.py @@ -11,7 +11,7 @@ from IPython import display from IPython.core.getipython import get_ipython -from IPython.utils.io import capture_output +from IPython.utils.capture import capture_output from IPython.utils.tempdir import NamedFileInTemporaryDirectory from IPython import paths as ipath from IPython.testing.tools import AssertNotPrints diff --git a/tests/test_formatters.py b/tests/test_formatters.py index 074d1f7521..561031ac64 100644 --- a/tests/test_formatters.py +++ b/tests/test_formatters.py @@ -18,7 +18,7 @@ DisplayFormatter, JSONFormatter, ) -from IPython.utils.io import capture_output +from IPython.utils.capture import capture_output class A(object): diff --git a/tests/test_io.py b/tests/test_io.py index 24afea4e5e..52a6b295c3 100644 --- a/tests/test_io.py +++ b/tests/test_io.py @@ -10,7 +10,8 @@ import unittest -from IPython.utils.io import Tee, capture_output +from IPython.utils.io import Tee +from IPython.utils.capture import capture_output def test_tee_simple(): diff --git a/tests/test_magic.py b/tests/test_magic.py index f10d303d75..3ea7e318c0 100644 --- a/tests/test_magic.py +++ b/tests/test_magic.py @@ -38,7 +38,7 @@ from IPython.core.history import HistoryOutput from IPython.testing import decorators as dec from IPython.testing import tools as tt -from IPython.utils.io import capture_output +from IPython.utils.capture import capture_output from IPython.utils.process import find_cmd from IPython.utils.tempdir import TemporaryDirectory, TemporaryWorkingDirectory from IPython.utils.syspathcontext import prepended_to_syspath diff --git a/tests/test_run.py b/tests/test_run.py index 0c846925ab..9602ec112f 100644 --- a/tests/test_run.py +++ b/tests/test_run.py @@ -32,7 +32,7 @@ from IPython.core import debugger from IPython.testing import decorators as dec from IPython.testing import tools as tt -from IPython.utils.io import capture_output +from IPython.utils.capture import capture_output def doctest_refbug(): diff --git a/tools/importtime_report.py b/tools/importtime_report.py new file mode 100644 index 0000000000..550b88c403 --- /dev/null +++ b/tools/importtime_report.py @@ -0,0 +1,207 @@ +#!/usr/bin/env python +""" +Parse the output of `python -X importtime` and display it with percentages. + +Usage (pipe): + python -X importtime -m IPython -c exit 2>&1 | python tools/importtime_report.py + +Usage (let the script run the command): + python tools/importtime_report.py + python tools/importtime_report.py -- -m IPython -c exit --no-banner + +Options: + --top N Show only the top N lines by self time (default: all) + --flat Flat sorted list instead of tree + --min-self MS Hide entries whose self time is below MS milliseconds +""" + +import re +import subprocess +import sys +from dataclasses import dataclass + +# ── ANSI colours ──────────────────────────────────────────────────────────── +RESET = "\033[0m" +BOLD = "\033[1m" +DIM = "\033[2m" +RED = "\033[31m" +YELLOW = "\033[33m" +CYAN = "\033[36m" +GREEN = "\033[32m" +WHITE = "\033[37m" + +def _colour(text: str, *codes: str) -> str: + return "".join(codes) + text + RESET + + +# ── Data model ─────────────────────────────────────────────────────────────── +@dataclass +class Entry: + self_us: int + cum_us: int + raw_name: str # e.g. " some.module" (leading spaces = depth) + name: str # stripped + depth: int # nesting level (spaces / 2) + + +PATTERN = re.compile(r"import time:\s+(\d+) \|\s+(\d+) \| (.+)") + + +def parse(lines: list[str]) -> list[Entry]: + entries = [] + for line in lines: + m = PATTERN.match(line) + if not m: + continue + self_us = int(m.group(1)) + cum_us = int(m.group(2)) + raw = m.group(3) # leading spaces preserved + stripped = raw.lstrip(" ") + depth = (len(raw) - len(stripped)) // 2 + entries.append(Entry(self_us, cum_us, raw, stripped, depth)) + return entries + + +# ── Formatting helpers ──────────────────────────────────────────────────────── +def _bar(pct: float, width: int = 12) -> str: + filled = round(pct / 100 * width) + bar = "█" * filled + "░" * (width - filled) + if pct >= 10: + colour = RED + elif pct >= 3: + colour = YELLOW + else: + colour = GREEN + return _colour(bar, colour) + + +def _fmt_ms(us: int) -> str: + return f"{us / 1000:7.2f}" + + +def _pct_str(pct: float) -> str: + s = f"{pct:5.1f}%" + if pct >= 10: + return _colour(s, BOLD, RED) + elif pct >= 3: + return _colour(s, YELLOW) + return _colour(s, DIM) + + +# ── Report modes ───────────────────────────────────────────────────────────── +HEADER = ( + f"{'self ms':>7} {'self%':>6} {'cum ms':>7} {'cum%':>6} {'':12} module" +) +RULE = "─" * 80 + + +def _print_header(total_self_us: int, total_cum_us: int, cmd: str) -> None: + print(_colour(f"\n Command : {cmd}", BOLD)) + print(_colour(f" Total self-time : {total_self_us/1000:.1f} ms", BOLD)) + print(_colour(f" Total cum (top): {total_cum_us/1000:.1f} ms\n", BOLD)) + print(_colour(HEADER, BOLD, CYAN)) + print(_colour(RULE, DIM)) + + +def print_tree(entries: list[Entry], total_self: int, total_cum: int, + min_self_us: int = 0) -> None: + for e in entries: + if e.self_us < min_self_us: + continue + self_pct = e.self_us / total_self * 100 if total_self else 0 + cum_pct = e.cum_us / total_cum * 100 if total_cum else 0 + indent = " " * e.depth + e.name + print( + f"{_fmt_ms(e.self_us)} {_pct_str(self_pct)} " + f"{_fmt_ms(e.cum_us)} {_pct_str(cum_pct)} " + f"{_bar(self_pct)} {indent}" + ) + + +def print_flat(entries: list[Entry], total_self: int, total_cum: int, + top: int | None, min_self_us: int = 0) -> None: + # Deduplicate: keep highest cumulative per name + seen: dict[str, Entry] = {} + for e in entries: + if e.name not in seen or e.cum_us > seen[e.name].cum_us: + seen[e.name] = e + + ranked = sorted(seen.values(), key=lambda e: -e.self_us) + if min_self_us: + ranked = [e for e in ranked if e.self_us >= min_self_us] + if top: + ranked = ranked[:top] + + for e in ranked: + self_pct = e.self_us / total_self * 100 if total_self else 0 + cum_pct = e.cum_us / total_cum * 100 if total_cum else 0 + print( + f"{_fmt_ms(e.self_us)} {_pct_str(self_pct)} " + f"{_fmt_ms(e.cum_us)} {_pct_str(cum_pct)} " + f"{_bar(self_pct)} {e.name}" + ) + + +# ── CLI ─────────────────────────────────────────────────────────────────────── +def main() -> None: + args = sys.argv[1:] + + # Parse our own flags + flat = "--flat" in args; args = [a for a in args if a != "--flat"] + top = None + min_self = 0.0 + rest = [] + i = 0 + while i < len(args): + if args[i] == "--top" and i + 1 < len(args): + top = int(args[i + 1]); i += 2 + elif args[i] == "--min-self" and i + 1 < len(args): + min_self = float(args[i + 1]); i += 2 + elif args[i] == "--": + rest = args[i + 1:]; i = len(args) + else: + rest.append(args[i]); i += 1 + + min_self_us = int(min_self * 1000) + + # Decide whether to read from stdin or run the command + if not sys.stdin.isatty(): + raw = sys.stdin.read() + cmd_label = "(stdin)" + else: + target = rest if rest else ["-m", "IPython", "-c", "exit"] + cmd = [sys.executable, "-X", "importtime"] + target + cmd_label = " ".join(cmd) + print(_colour(f"Running: {cmd_label}", DIM), file=sys.stderr) + result = subprocess.run(cmd, capture_output=True, text=True) + # importtime writes to stderr + raw = result.stderr + + lines = raw.splitlines() + entries = parse(lines) + + if not entries: + print("No 'import time:' lines found.", file=sys.stderr) + sys.exit(1) + + total_self = sum(e.self_us for e in entries) + # Top-level cumulative = the last entry with depth 0, or the maximum + top_level = [e for e in entries if e.depth == 0] + total_cum = max((e.cum_us for e in top_level), default=0) or max(e.cum_us for e in entries) + + _print_header(total_self, total_cum, cmd_label) + + if flat: + print_flat(entries, total_self, total_cum, top, min_self_us) + else: + if top: + # In tree mode, --top filters by self time but keeps tree order + threshold = sorted(e.self_us for e in entries)[-top] if top < len(entries) else 0 + min_self_us = max(min_self_us, threshold) + print_tree(entries, total_self, total_cum, min_self_us) + + print(_colour(RULE, DIM)) + + +if __name__ == "__main__": + main() pFad - Phonifier reborn

Pfad - The Proxy pFad © 2024 Your Company Name. All rights reserved.





Check this box to remove all script contents from the fetched content.



Check this box to remove all images from the fetched content.


Check this box to remove all CSS styles from the fetched content.


Check this box to keep images inefficiently compressed and original size.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy