# -*- coding: utf-8 -*-
#
# Copyright © Spyder Project Contributors
# based on pylintgui.py by Pierre Raybaut
#
# Licensed under the terms of the MIT License
# (see spyder/__init__.py for details)
"""
Profiler widget.
See the official documentation on python profiling:
https://docs.python.org/3/library/profile.html
"""
# Standard library imports
import logging
import os
import os.path as osp
import re
import sys
import time
from itertools import islice
# Third party imports
from qtpy.compat import getopenfilename, getsavefilename
from qtpy.QtCore import QByteArray, QProcess, QProcessEnvironment, Qt, Signal
from qtpy.QtGui import QColor
from qtpy.QtWidgets import (QApplication, QLabel, QMessageBox, QTreeWidget,
QTreeWidgetItem, QVBoxLayout)
# Local imports
from spyder.api.translations import get_translation
from spyder.api.widgets.main_widget import PluginMainWidget
from spyder.api.widgets.mixins import SpyderWidgetMixin
from spyder.config.base import get_conf_path
from spyder.plugins.variableexplorer.widgets.texteditor import TextEditor
from spyder.py3compat import to_text_string
from spyder.utils.misc import add_pathlist_to_PYTHONPATH, getcwd_or_home
from spyder.utils.palette import SpyderPalette, QStylePalette
from spyder.utils.programs import shell_split
from spyder.utils.qthelpers import get_item_user_text, set_item_user_text
from spyder.widgets.comboboxes import PythonModulesComboBox
# Localization
_ = get_translation('spyder')
# Logging
logger = logging.getLogger(__name__)
# --- Constants
# ----------------------------------------------------------------------------
MAIN_TEXT_COLOR = QStylePalette.COLOR_TEXT_1
class ProfilerWidgetActions:
# Triggers
Browse = 'browse_action'
Clear = 'clear_action'
Collapse = 'collapse_action'
Expand = 'expand_action'
LoadData = 'load_data_action'
Run = 'run_action'
SaveData = 'save_data_action'
ShowOutput = 'show_output_action'
class ProfilerWidgetToolbars:
Information = 'information_toolbar'
class ProfilerWidgetMainToolbarSections:
Main = 'main_section'
class ProfilerWidgetInformationToolbarSections:
Main = 'main_section'
class ProfilerWidgetMainToolbarItems:
FileCombo = 'file_combo'
class ProfilerWidgetInformationToolbarItems:
Stretcher1 = 'stretcher_1'
Stretcher2 = 'stretcher_2'
DateLabel = 'date_label'
# --- Utils
# ----------------------------------------------------------------------------
def is_profiler_installed():
from spyder.utils.programs import is_module_installed
return is_module_installed('cProfile') and is_module_installed('pstats')
def gettime_s(text):
"""
Parse text and return a time in seconds.
The text is of the format 0h : 0.min:0.0s:0 ms:0us:0 ns.
Spaces are not taken into account and any of the specifiers can be ignored.
"""
pattern = r'([+-]?\d+\.?\d*) ?([mμnsinh]+)'
matches = re.findall(pattern, text)
if len(matches) == 0:
return None
time = 0.
for res in matches:
tmp = float(res[0])
if res[1] == 'ns':
tmp *= 1e-9
elif res[1] == u'\u03BCs':
tmp *= 1e-6
elif res[1] == 'ms':
tmp *= 1e-3
elif res[1] == 'min':
tmp *= 60
elif res[1] == 'h':
tmp *= 3600
time += tmp
return time
# --- Widgets
# ----------------------------------------------------------------------------
class ProfilerWidget(PluginMainWidget):
"""
Profiler widget.
"""
ENABLE_SPINNER = True
DATAPATH = get_conf_path('profiler.results')
# --- Signals
# ------------------------------------------------------------------------
sig_edit_goto_requested = Signal(str, int, str)
"""
This signal will request to open a file in a given row and column
using a code editor.
Parameters
----------
path: str
Path to file.
row: int
Cursor starting row position.
word: str
Word to select on given row.
"""
sig_redirect_stdio_requested = Signal(bool)
"""
This signal is emitted to request the main application to redirect
standard output/error when using Open/Save/Browse dialogs within widgets.
Parameters
----------
redirect: bool
Start redirect (True) or stop redirect (False).
"""
sig_started = Signal()
"""This signal is emitted to inform the profiling process has started."""
sig_finished = Signal()
"""This signal is emitted to inform the profile profiling has finished."""
def __init__(self, name=None, plugin=None, parent=None):
super().__init__(name, plugin, parent)
self.set_conf('text_color', MAIN_TEXT_COLOR)
# Attributes
self._last_wdir = None
self._last_args = None
self._last_pythonpath = None
self.error_output = None
self.output = None
self.running = False
self.text_color = self.get_conf('text_color')
# Widgets
self.process = None
self.filecombo = PythonModulesComboBox(
self, id_=ProfilerWidgetMainToolbarItems.FileCombo)
self.datatree = ProfilerDataTree(self)
self.datelabel = QLabel()
self.datelabel.ID = ProfilerWidgetInformationToolbarItems.DateLabel
# Layout
layout = QVBoxLayout()
layout.addWidget(self.datatree)
self.setLayout(layout)
# Signals
self.datatree.sig_edit_goto_requested.connect(
self.sig_edit_goto_requested)
# --- PluginMainWidget API
# ------------------------------------------------------------------------
def get_title(self):
return _('Profiler')
def get_focus_widget(self):
return self.datatree
def setup(self):
self.start_action = self.create_action(
ProfilerWidgetActions.Run,
text=_("Run profiler"),
tip=_("Run profiler"),
icon=self.create_icon('run'),
triggered=self.run,
)
browse_action = self.create_action(
ProfilerWidgetActions.Browse,
text='',
tip=_('Select Python script'),
icon=self.create_icon('fileopen'),
triggered=lambda x: self.select_file(),
)
self.log_action = self.create_action(
ProfilerWidgetActions.ShowOutput,
text=_("Output"),
tip=_("Show program's output"),
icon=self.create_icon('log'),
triggered=self.show_log,
)
self.collapse_action = self.create_action(
ProfilerWidgetActions.Collapse,
text=_('Collapse'),
tip=_('Collapse one level up'),
icon=self.create_icon('collapse'),
triggered=lambda x=None: self.datatree.change_view(-1),
)
self.expand_action = self.create_action(
ProfilerWidgetActions.Expand,
text=_('Expand'),
tip=_('Expand one level down'),
icon=self.create_icon('expand'),
triggered=lambda x=None: self.datatree.change_view(1),
)
self.save_action = self.create_action(
ProfilerWidgetActions.SaveData,
text=_("Save data"),
tip=_('Save profiling data'),
icon=self.create_icon('filesave'),
triggered=self.save_data,
)
self.load_action = self.create_action(
ProfilerWidgetActions.LoadData,
text=_("Load data"),
tip=_('Load profiling data for comparison'),
icon=self.create_icon('fileimport'),
triggered=self.compare,
)
self.clear_action = self.create_action(
ProfilerWidgetActions.Clear,
text=_("Clear comparison"),
tip=_("Clear comparison"),
icon=self.create_icon('editdelete'),
triggered=self.clear,
)
self.clear_action.setEnabled(False)
# Main Toolbar
toolbar = self.get_main_toolbar()
for item in [self.filecombo, browse_action, self.start_action]:
self.add_item_to_toolbar(
item,
toolbar=toolbar,
section=ProfilerWidgetMainToolbarSections.Main,
)
# Secondary Toolbar
secondary_toolbar = self.create_toolbar(
ProfilerWidgetToolbars.Information)
for item in [self.collapse_action, self.expand_action,
self.create_stretcher(
id_=ProfilerWidgetInformationToolbarItems.Stretcher1),
self.datelabel,
self.create_stretcher(
id_=ProfilerWidgetInformationToolbarItems.Stretcher2),
self.log_action,
self.save_action, self.load_action, self.clear_action]:
self.add_item_to_toolbar(
item,
toolbar=secondary_toolbar,
section=ProfilerWidgetInformationToolbarSections.Main,
)
# Setup
if not is_profiler_installed():
# This should happen only on certain GNU/Linux distributions
# or when this a home-made Python build because the Python
# profilers are included in the Python standard library
for widget in (self.datatree, self.filecombo,
self.start_action):
widget.setDisabled(True)
url = 'https://docs.python.org/3/library/profile.html'
text = '%s %s' % (_('Please install'), url,
_("the Python profiler modules"))
self.datelabel.setText(text)
def update_actions(self):
if self.running:
icon = self.create_icon('stop')
else:
icon = self.create_icon('run')
self.start_action.setIcon(icon)
self.start_action.setEnabled(bool(self.filecombo.currentText()))
# --- Private API
# ------------------------------------------------------------------------
def _kill_if_running(self):
"""Kill the profiling process if it is running."""
if self.process is not None:
if self.process.state() == QProcess.Running:
self.process.kill()
self.process.waitForFinished()
self.update_actions()
def _finished(self, exit_code, exit_status):
"""
Parse results once the profiling process has ended.
Parameters
----------
exit_code: int
QProcess exit code.
exit_status: str
QProcess exit status.
"""
self.running = False
self.show_errorlog() # If errors occurred, show them.
self.output = self.error_output + self.output
self.datelabel.setText('')
self.show_data(justanalyzed=True)
self.update_actions()
def _read_output(self, error=False):
"""
Read otuput from QProcess.
Parameters
----------
error: bool, optional
Process QProcess output or error channels. Default is False.
"""
if error:
self.process.setReadChannel(QProcess.StandardError)
else:
self.process.setReadChannel(QProcess.StandardOutput)
qba = QByteArray()
while self.process.bytesAvailable():
if error:
qba += self.process.readAllStandardError()
else:
qba += self.process.readAllStandardOutput()
text = to_text_string(qba.data(), encoding='utf-8')
if error:
self.error_output += text
else:
self.output += text
# --- Public API
# ------------------------------------------------------------------------
def save_data(self):
"""Save data."""
title = _( "Save profiler result")
filename, _selfilter = getsavefilename(
self,
title,
getcwd_or_home(),
_("Profiler result") + " (*.Result)",
)
if filename:
self.datatree.save_data(filename)
def compare(self):
"""Compare previous saved run with last run."""
filename, _selfilter = getopenfilename(
self,
_("Select script to compare"),
getcwd_or_home(),
_("Profiler result") + " (*.Result)",
)
if filename:
self.datatree.compare(filename)
self.show_data()
self.clear_action.setEnabled(True)
def clear(self):
"""Clear data in tree."""
self.datatree.compare(None)
self.datatree.hide_diff_cols(True)
self.show_data()
self.clear_action.setEnabled(False)
def analyze(self, filename, wdir=None, args=None, pythonpath=None):
"""
Start the profiling process.
Parameters
----------
wdir: str
Working directory path string. Default is None.
args: list
Arguments to pass to the profiling process. Default is None.
pythonpath: str
Python path string. Default is None.
"""
if not is_profiler_installed():
return
self._kill_if_running()
# TODO: storing data is not implemented yet
# index, _data = self.get_data(filename)
combo = self.filecombo
items = [combo.itemText(idx) for idx in range(combo.count())]
index = None
if index is None and filename not in items:
self.filecombo.addItem(filename)
self.filecombo.setCurrentIndex(self.filecombo.count() - 1)
else:
self.filecombo.setCurrentIndex(self.filecombo.findText(filename))
self.filecombo.selected()
if self.filecombo.is_valid():
if wdir is None:
wdir = osp.dirname(filename)
self.start(wdir, args, pythonpath)
def select_file(self, filename=None):
"""
Select filename to profile.
Parameters
----------
filename: str, optional
Path to filename to profile. default is None.
Notes
-----
If no `filename` is provided an open filename dialog will be used.
"""
if filename is None:
self.sig_redirect_stdio_requested.emit(False)
filename, _selfilter = getopenfilename(
self,
_("Select Python script"),
getcwd_or_home(),
_("Python scripts") + " (*.py ; *.pyw)"
)
self.sig_redirect_stdio_requested.emit(True)
if filename:
self.analyze(filename)
def show_log(self):
"""Show process output log."""
if self.output:
output_dialog = TextEditor(
self.output,
title=_("Profiler output"),
readonly=True,
parent=self,
)
output_dialog.resize(700, 500)
output_dialog.exec_()
def show_errorlog(self):
"""Show process error log."""
if self.error_output:
output_dialog = TextEditor(
self.error_output,
title=_("Profiler output"),
readonly=True,
parent=self,
)
output_dialog.resize(700, 500)
output_dialog.exec_()
def start(self, wdir=None, args=None, pythonpath=None):
"""
Start the profiling process.
Parameters
----------
wdir: str
Working directory path string. Default is None.
args: list
Arguments to pass to the profiling process. Default is None.
pythonpath: str
Python path string. Default is None.
"""
filename = to_text_string(self.filecombo.currentText())
if wdir is None:
wdir = self._last_wdir
if wdir is None:
wdir = osp.basename(filename)
if args is None:
args = self._last_args
if args is None:
args = []
if pythonpath is None:
pythonpath = self._last_pythonpath
self._last_wdir = wdir
self._last_args = args
self._last_pythonpath = pythonpath
self.datelabel.setText(_('Profiling, please wait...'))
self.process = QProcess(self)
self.process.setProcessChannelMode(QProcess.SeparateChannels)
self.process.setWorkingDirectory(wdir)
self.process.readyReadStandardOutput.connect(self._read_output)
self.process.readyReadStandardError.connect(
lambda: self._read_output(error=True))
self.process.finished.connect(
lambda ec, es=QProcess.ExitStatus: self._finished(ec, es))
self.process.finished.connect(self.stop_spinner)
if pythonpath is not None:
env = [to_text_string(_pth)
for _pth in self.process.systemEnvironment()]
add_pathlist_to_PYTHONPATH(env, pythonpath)
processEnvironment = QProcessEnvironment()
for envItem in env:
envName, __, envValue = envItem.partition('=')
processEnvironment.insert(envName, envValue)
processEnvironment.insert("PYTHONIOENCODING", "utf8")
self.process.setProcessEnvironment(processEnvironment)
self.output = ''
self.error_output = ''
self.running = True
self.start_spinner()
p_args = ['-m', 'cProfile', '-o', self.DATAPATH]
if os.name == 'nt':
# On Windows, one has to replace backslashes by slashes to avoid
# confusion with escape characters (otherwise, for example, '\t'
# will be interpreted as a tabulation):
p_args.append(osp.normpath(filename).replace(os.sep, '/'))
else:
p_args.append(filename)
if args:
p_args.extend(shell_split(args))
executable = sys.executable
if executable.endswith("spyder.exe"):
# py2exe distribution
executable = "python.exe"
self.process.start(executable, p_args)
running = self.process.waitForStarted()
if not running:
QMessageBox.critical(
self,
_("Error"),
_("Process failed to start"),
)
self.update_actions()
def stop(self):
"""Stop the running process."""
self.running = False
self.process.kill()
self.stop_spinner()
self.update_actions()
def run(self):
"""Toggle starting or running the profiling process."""
if self.running:
self.stop()
else:
self.start()
def show_data(self, justanalyzed=False):
"""
Show analyzed data on results tree.
Parameters
----------
justanalyzed: bool, optional
Default is False.
"""
if not justanalyzed:
self.output = None
self.log_action.setEnabled(self.output is not None
and len(self.output) > 0)
self._kill_if_running()
filename = to_text_string(self.filecombo.currentText())
if not filename:
return
self.datelabel.setText(_('Sorting data, please wait...'))
QApplication.processEvents()
self.datatree.load_data(self.DATAPATH)
self.datatree.show_tree()
text_style = "%s "
date_text = text_style % (self.text_color,
time.strftime("%Y-%m-%d %H:%M:%S",
time.localtime()))
self.datelabel.setText(date_text)
class TreeWidgetItem( QTreeWidgetItem ):
def __init__(self, parent=None):
QTreeWidgetItem.__init__(self, parent)
def __lt__(self, otherItem):
column = self.treeWidget().sortColumn()
try:
if column == 1 or column == 3: # TODO: Hardcoded Column
t0 = gettime_s(self.text(column))
t1 = gettime_s(otherItem.text(column))
if t0 is not None and t1 is not None:
return t0 > t1
return float( self.text(column) ) > float( otherItem.text(column) )
except ValueError:
return self.text(column) > otherItem.text(column)
class ProfilerDataTree(QTreeWidget, SpyderWidgetMixin):
"""
Convenience tree widget (with built-in model)
to store and view profiler data.
The quantities calculated by the profiler are as follows
(from profile.Profile):
[0] = The number of times this function was called, not counting direct
or indirect recursion,
[1] = Number of times this function appears on the stack, minus one
[2] = Total time spent internal to this function
[3] = Cumulative time that this function was present on the stack. In
non-recursive functions, this is the total execution time from start
to finish of each invocation of a function, including time spent in
all subfunctions.
[4] = A dictionary indicating for each function name, the number of times
it was called by us.
"""
SEP = r"<[=]>" # separator between filename and linenumber
# (must be improbable as a filename to avoid splitting the filename itself)
# Signals
sig_edit_goto_requested = Signal(str, int, str)
def __init__(self, parent=None):
super().__init__(parent, class_parent=parent)
self.header_list = [_('Function/Module'), _('Total Time'), _('Diff'),
_('Local Time'), _('Diff'), _('Calls'), _('Diff'),
_('File:line')]
self.icon_list = {
'module': self.create_icon('python'),
'function': self.create_icon('function'),
'builtin': self.create_icon('python'),
'constructor': self.create_icon('class')
}
self.profdata = None # To be filled by self.load_data()
self.stats = None # To be filled by self.load_data()
self.item_depth = None
self.item_list = None
self.items_to_be_shown = None
self.current_view_depth = None
self.compare_file = None
self.setColumnCount(len(self.header_list))
self.setHeaderLabels(self.header_list)
self.initialize_view()
self.itemActivated.connect(self.item_activated)
self.itemExpanded.connect(self.item_expanded)
def set_item_data(self, item, filename, line_number):
"""Set tree item user data: filename (string) and line_number (int)"""
set_item_user_text(item, '%s%s%d' % (filename, self.SEP, line_number))
def get_item_data(self, item):
"""Get tree item user data: (filename, line_number)"""
filename, line_number_str = get_item_user_text(item).split(self.SEP)
return filename, int(line_number_str)
def initialize_view(self):
"""Clean the tree and view parameters"""
self.clear()
self.item_depth = 0 # To be use for collapsing/expanding one level
self.item_list = [] # To be use for collapsing/expanding one level
self.items_to_be_shown = {}
self.current_view_depth = 0
def load_data(self, profdatafile):
"""Load profiler data saved by profile/cProfile module"""
import pstats
# Fixes spyder-ide/spyder#6220.
try:
stats_indi = [pstats.Stats(profdatafile), ]
except (OSError, IOError):
self.profdata = None
return
self.profdata = stats_indi[0]
if self.compare_file is not None:
# Fixes spyder-ide/spyder#5587.
try:
stats_indi.append(pstats.Stats(self.compare_file))
except (OSError, IOError) as e:
QMessageBox.critical(
self, _("Error"),
_("Error when trying to load profiler results. "
"The error was
"
"{0}").format(e))
self.compare_file = None
map(lambda x: x.calc_callees(), stats_indi)
self.profdata.calc_callees()
self.stats1 = stats_indi
self.stats = stats_indi[0].stats
def compare(self,filename):
self.hide_diff_cols(False)
self.compare_file = filename
def hide_diff_cols(self, hide):
for i in (2,4,6):
self.setColumnHidden(i, hide)
def save_data(self, filename):
"""Save profiler data."""
self.stats1[0].dump_stats(filename)
def find_root(self):
"""Find a function without a caller"""
# Fixes spyder-ide/spyder#8336.
if self.profdata is not None:
self.profdata.sort_stats("cumulative")
else:
return
for func in self.profdata.fcn_list:
if ('~', 0) != func[0:2] and not func[2].startswith(
''):
# This skips the profiler function at the top of the list
# it does only occur in Python 3
return func
def find_callees(self, parent):
"""Find all functions called by (parent) function."""
# FIXME: This implementation is very inneficient, because it
# traverses all the data to find children nodes (callees)
return self.profdata.all_callees[parent]
def show_tree(self):
"""Populate the tree with profiler data and display it."""
self.initialize_view() # Clear before re-populating
self.setItemsExpandable(True)
self.setSortingEnabled(False)
rootkey = self.find_root() # This root contains profiler overhead
if rootkey is not None:
self.populate_tree(self, self.find_callees(rootkey))
self.resizeColumnToContents(0)
self.setSortingEnabled(True)
self.sortItems(1, Qt.AscendingOrder) # FIXME: hardcoded index
self.change_view(1)
def function_info(self, functionKey):
"""Returns processed information about the function's name and file."""
node_type = 'function'
filename, line_number, function_name = functionKey
if function_name == '':
modulePath, moduleName = osp.split(filename)
node_type = 'module'
if moduleName == '__init__.py':
modulePath, moduleName = osp.split(modulePath)
function_name = '<' + moduleName + '>'
if not filename or filename == '~':
file_and_line = '(built-in)'
node_type = 'builtin'
else:
if function_name == '__init__':
node_type = 'constructor'
file_and_line = '%s : %d' % (filename, line_number)
return filename, line_number, function_name, file_and_line, node_type
@staticmethod
def format_measure(measure):
"""Get format and units for data coming from profiler task."""
# Convert to a positive value.
measure = abs(measure)
# For number of calls
if isinstance(measure, int):
return to_text_string(measure)
# For time measurements
if 1.e-9 < measure <= 1.e-6:
measure = u"{0:.2f} ns".format(measure / 1.e-9)
elif 1.e-6 < measure <= 1.e-3:
measure = u"{0:.2f} \u03BCs".format(measure / 1.e-6)
elif 1.e-3 < measure <= 1:
measure = u"{0:.2f} ms".format(measure / 1.e-3)
elif 1 < measure <= 60:
measure = u"{0:.2f} s".format(measure)
elif 60 < measure <= 3600:
m, s = divmod(measure, 3600)
if s > 60:
m, s = divmod(measure, 60)
s = to_text_string(s).split(".")[-1]
measure = u"{0:.0f}.{1:.2s} min".format(m, s)
else:
h, m = divmod(measure, 3600)
if m > 60:
m /= 60
measure = u"{0:.0f}h:{1:.0f}min".format(h, m)
return measure
def color_string(self, x):
"""Return a string formatted delta for the values in x.
Args:
x: 2-item list of integers (representing number of calls) or
2-item list of floats (representing seconds of runtime).
Returns:
A list with [formatted x[0], [color, formatted delta]], where
color reflects whether x[1] is lower, greater, or the same as
x[0].
"""
diff_str = ""
color = "black"
if len(x) == 2 and self.compare_file is not None:
difference = x[0] - x[1]
if difference:
color, sign = ((SpyderPalette.COLOR_SUCCESS_1, '-')
if difference < 0
else (SpyderPalette.COLOR_ERROR_1, '+'))
diff_str = '{}{}'.format(sign, self.format_measure(difference))
return [self.format_measure(x[0]), [diff_str, color]]
def format_output(self, child_key):
""" Formats the data.
self.stats1 contains a list of one or two pstat.Stats() instances, with
the first being the current run and the second, the saved run, if it
exists. Each Stats instance is a dictionary mapping a function to
5 data points - cumulative calls, number of calls, total time,
cumulative time, and callers.
format_output() converts the number of calls, total time, and
cumulative time to a string format for the child_key parameter.
"""
data = [x.stats.get(child_key, [0, 0, 0, 0, {}]) for x in self.stats1]
return (map(self.color_string, islice(zip(*data), 1, 4)))
def populate_tree(self, parentItem, children_list):
"""Recursive method to create each item (and associated data) in the tree."""
for child_key in children_list:
self.item_depth += 1
(filename, line_number, function_name, file_and_line, node_type
) = self.function_info(child_key)
((total_calls, total_calls_dif), (loc_time, loc_time_dif), (cum_time,
cum_time_dif)) = self.format_output(child_key)
child_item = TreeWidgetItem(parentItem)
self.item_list.append(child_item)
self.set_item_data(child_item, filename, line_number)
# FIXME: indexes to data should be defined by a dictionary on init
child_item.setToolTip(0, _('Function or module name'))
child_item.setData(0, Qt.DisplayRole, function_name)
child_item.setIcon(0, self.icon_list[node_type])
child_item.setToolTip(1, _('Time in function '\
'(including sub-functions)'))
child_item.setData(1, Qt.DisplayRole, cum_time)
child_item.setTextAlignment(1, Qt.AlignRight)
child_item.setData(2, Qt.DisplayRole, cum_time_dif[0])
child_item.setForeground(2, QColor(cum_time_dif[1]))
child_item.setTextAlignment(2, Qt.AlignLeft)
child_item.setToolTip(3, _('Local time in function '\
'(not in sub-functions)'))
child_item.setData(3, Qt.DisplayRole, loc_time)
child_item.setTextAlignment(3, Qt.AlignRight)
child_item.setData(4, Qt.DisplayRole, loc_time_dif[0])
child_item.setForeground(4, QColor(loc_time_dif[1]))
child_item.setTextAlignment(4, Qt.AlignLeft)
child_item.setToolTip(5, _('Total number of calls '\
'(including recursion)'))
child_item.setData(5, Qt.DisplayRole, total_calls)
child_item.setTextAlignment(5, Qt.AlignRight)
child_item.setData(6, Qt.DisplayRole, total_calls_dif[0])
child_item.setForeground(6, QColor(total_calls_dif[1]))
child_item.setTextAlignment(6, Qt.AlignLeft)
child_item.setToolTip(7, _('File:line '\
'where function is defined'))
child_item.setData(7, Qt.DisplayRole, file_and_line)
#child_item.setExpanded(True)
if self.is_recursive(child_item):
child_item.setData(7, Qt.DisplayRole, '(%s)' % _('recursion'))
child_item.setDisabled(True)
else:
callees = self.find_callees(child_key)
if self.item_depth < 3:
self.populate_tree(child_item, callees)
elif callees:
child_item.setChildIndicatorPolicy(child_item.ShowIndicator)
self.items_to_be_shown[id(child_item)] = callees
self.item_depth -= 1
def item_activated(self, item):
filename, line_number = self.get_item_data(item)
self.sig_edit_goto_requested.emit(filename, line_number, '')
def item_expanded(self, item):
if item.childCount() == 0 and id(item) in self.items_to_be_shown:
callees = self.items_to_be_shown[id(item)]
self.populate_tree(item, callees)
def is_recursive(self, child_item):
"""Returns True is a function is a descendant of itself."""
ancestor = child_item.parent()
# FIXME: indexes to data should be defined by a dictionary on init
while ancestor:
if (child_item.data(0, Qt.DisplayRole
) == ancestor.data(0, Qt.DisplayRole) and
child_item.data(7, Qt.DisplayRole
) == ancestor.data(7, Qt.DisplayRole)):
return True
else:
ancestor = ancestor.parent()
return False
def get_top_level_items(self):
"""Iterate over top level items"""
return [self.topLevelItem(_i) for _i in range(self.topLevelItemCount())]
def get_items(self, maxlevel):
"""Return all items with a level <= `maxlevel`"""
itemlist = []
def add_to_itemlist(item, maxlevel, level=1):
level += 1
for index in range(item.childCount()):
citem = item.child(index)
itemlist.append(citem)
if level <= maxlevel:
add_to_itemlist(citem, maxlevel, level)
for tlitem in self.get_top_level_items():
itemlist.append(tlitem)
if maxlevel > 0:
add_to_itemlist(tlitem, maxlevel=maxlevel)
return itemlist
def change_view(self, change_in_depth):
"""Change the view depth by expand or collapsing all same-level nodes"""
self.current_view_depth += change_in_depth
if self.current_view_depth < 0:
self.current_view_depth = 0
self.collapseAll()
if self.current_view_depth > 0:
for item in self.get_items(maxlevel=self.current_view_depth-1):
item.setExpanded(True)
# =============================================================================
# Tests
# =============================================================================
def primes(n):
"""
Simple test function
Taken from http://www.huyng.com/posts/python-performance-analysis/
"""
if n==2:
return [2]
elif n<2:
return []
s=list(range(3,n+1,2))
mroot = n ** 0.5
half=(n+1)//2-1
i=0
m=3
while m <= mroot:
if s[i]:
j=(m*m-3)//2
s[j]=0
while j