├── spyder_line_profiler ├── tests │ ├── __init__.py │ └── test_lineprofiler.py ├── example │ ├── __init__.py │ ├── subdir │ │ ├── __init__.py │ │ └── profiling_test_script2.py │ └── profiling_test_script.py ├── spyder │ ├── __init__.py │ ├── config.py │ ├── run_conf.py │ ├── confpage.py │ ├── plugin.py │ └── widgets.py └── __init__.py ├── .github ├── FUNDING.yml ├── PULL_REQUEST_TEMPLATE.md ├── scripts │ └── generate-without-spyder.py └── workflows │ └── run-tests.yml ├── requirements ├── tests.txt └── conda.txt ├── MANIFEST.in ├── img_src └── screenshot_profiler.png ├── codecov.yml ├── AUTHORS.md ├── .gitignore ├── LICENSE.txt ├── CONTRIBUTING.md ├── RELEASE.md ├── README.md ├── setup.py └── CHANGELOG.md /spyder_line_profiler/tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /spyder_line_profiler/example/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | open_collective: spyder 2 | -------------------------------------------------------------------------------- /requirements/tests.txt: -------------------------------------------------------------------------------- 1 | pytest 2 | pytest-qt 3 | -------------------------------------------------------------------------------- /spyder_line_profiler/example/subdir/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /requirements/conda.txt: -------------------------------------------------------------------------------- 1 | line_profiler 2 | spyder>=6.1,<6.2 3 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include CHANGELOG.md LICENSE.txt README.md 2 | recursive-include spyder_line_profiler * 3 | -------------------------------------------------------------------------------- /img_src/screenshot_profiler.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spyder-ide/spyder-line-profiler/HEAD/img_src/screenshot_profiler.png -------------------------------------------------------------------------------- /codecov.yml: -------------------------------------------------------------------------------- 1 | # See https://docs.codecov.io/docs/codecovyml-reference 2 | 3 | coverage: 4 | status: 5 | project: 6 | default: 7 | threshold: 5% # allow for 5% decrease in total coverage 8 | patch: 9 | default: 10 | target: 50% # require 50% of diff to be covered 11 | 12 | comment: 13 | layout: "files" 14 | -------------------------------------------------------------------------------- /AUTHORS.md: -------------------------------------------------------------------------------- 1 | # Authors 2 | 3 | ## Original author 4 | 5 | * Joseph Martinot-Lagarde ([@Nodd](http://github.com/Nodd)) 6 | 7 | 8 | ## Current maintainers 9 | 10 | * Jitse Niesen ([@jitseniesen](https://github.com/jitseniesen)) 11 | * The [Spyder Development Team](https://github.com/spyder-ide) 12 | * The [spyder-line-profiler Contributors](https://github.com/spyder-ide/spyder-line-profiler/graphs/contributors) 13 | -------------------------------------------------------------------------------- /spyder_line_profiler/spyder/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # ----------------------------------------------------------------------------- 4 | # Copyright (c) 2013- Spyder Project Contributors 5 | # 6 | # Released under the terms of the MIT License 7 | # (see LICENSE.txt in the project root directory for details) 8 | # -----------------------------------------------------------------------------""" 9 | 10 | """ 11 | Spyder Line Profiler 12 | """ 13 | -------------------------------------------------------------------------------- /spyder_line_profiler/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # ----------------------------------------------------------------------------- 4 | # Copyright (c) 2013- Spyder Project Contributors 5 | # 6 | # Released under the terms of the MIT License 7 | # (see LICENSE.txt in the project root directory for details) 8 | # ----------------------------------------------------------------------------- 9 | 10 | """ 11 | Spyder Line Profiler. 12 | """ 13 | 14 | __version__ = "0.4.3.dev0" 15 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.py[cod] 2 | 3 | # C extensions 4 | *.so 5 | 6 | # Packages 7 | *.egg 8 | *.egg-info 9 | dist 10 | build 11 | eggs 12 | parts 13 | bin 14 | var 15 | sdist 16 | develop-eggs 17 | .installed.cfg 18 | lib 19 | lib64 20 | __pycache__ 21 | 22 | # Installer logs 23 | pip-log.txt 24 | 25 | # Unit test / coverage reports 26 | .coverage 27 | .tox 28 | nosetests.xml 29 | 30 | # Translations 31 | *.mo 32 | 33 | # Mr Developer 34 | .mr.developer.cfg 35 | .project 36 | .pydevproject 37 | 38 | # Linux files 39 | .directory -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ## Description of Changes 2 | 3 | 4 | 5 | 6 | ### Affirmation 7 | 8 | By submitting this Pull Request or typing my (user)name below, 9 | I affirm the [Developer Certificate of Origin](https://developercertificate.org) 10 | with respect to all commits and content included in this PR, 11 | and understand I am releasing the same under Spyder's MIT (Expat) license. 12 | 13 | 14 | I certify the above statement is true and correct: 15 | 16 | 17 | -------------------------------------------------------------------------------- /.github/scripts/generate-without-spyder.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # 3 | # Copyright (c) Spyder Project Contributors 4 | # Licensed under the terms of the MIT License 5 | # (see LICENSE.txt for details) 6 | 7 | """Script to generate requirements/without-spyder.txt""" 8 | 9 | import re 10 | from pathlib import Path 11 | 12 | rootdir = Path(__file__).parents[2] 13 | input_filename = rootdir / 'requirements' / 'conda.txt' 14 | output_filename = rootdir / 'requirements' / 'without-spyder.txt' 15 | 16 | with open(input_filename) as infile: 17 | with open(output_filename, 'w') as outfile: 18 | for line in infile: 19 | package_name = re.match('[-a-z0-9_]*', line).group(0) 20 | if package_name != 'spyder': 21 | outfile.write(line) 22 | -------------------------------------------------------------------------------- /spyder_line_profiler/example/subdir/profiling_test_script2.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- coding: utf-8 -*- 3 | # 4 | # ----------------------------------------------------------------------------- 5 | # Copyright (c) 2013- Spyder Project Contributors 6 | # 7 | # Released under the terms of the MIT License 8 | # (see LICENSE.txt in the project root directory for details) 9 | # ----------------------------------------------------------------------------- 10 | 11 | from __future__ import ( 12 | print_function, division, unicode_literals, absolute_import) 13 | 14 | 15 | @profile 16 | def fact2(n): 17 | result = 1 18 | for i in range(2, n + 1): 19 | result *= i * 2 20 | return result 21 | 22 | 23 | def sum2(n): 24 | result = 0 25 | for i in range(1, n + 1): 26 | result += i * 2 27 | return result 28 | 29 | if __name__ == "__main__": 30 | print(fact2(120)) 31 | print(sum2(120)) 32 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2013 Spyder Project Contributors 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /spyder_line_profiler/spyder/config.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # ----------------------------------------------------------------------------- 4 | # Copyright (c) 2022- Spyder Project Contributors 5 | # 6 | # Released under the terms of the MIT License 7 | # (see LICENSE.txt in the project root directory for details) 8 | # ----------------------------------------------------------------------------- 9 | 10 | """Spyder line-profiler default configuration.""" 11 | 12 | CONF_SECTION = 'spyder_line_profiler' 13 | 14 | CONF_DEFAULTS = [ 15 | (CONF_SECTION, 16 | { 17 | 'use_colors': True, 18 | } 19 | ), 20 | ('shortcuts', 21 | { 22 | 'spyder_line_profiler/Run file in spyder_line_profiler': 'Shift+F10', 23 | } 24 | ) 25 | ] 26 | 27 | # IMPORTANT NOTES: 28 | # 1. If you want to *change* the default value of a current option, you need to 29 | # do a MINOR update in config version, e.g. from 1.0.0 to 1.1.0 30 | # 2. If you want to *remove* options that are no longer needed in our codebase, 31 | # or if you want to *rename* options, then you need to do a MAJOR update in 32 | # version, e.g. from 1.0.0 to 2.0.0 33 | # 3. You don't need to touch this value if you're just adding a new option 34 | CONF_VERSION = '2.0.0' 35 | -------------------------------------------------------------------------------- /spyder_line_profiler/example/profiling_test_script.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- coding: utf-8 -*- 3 | # 4 | # ----------------------------------------------------------------------------- 5 | # Copyright (c) 2013- Spyder Project Contributors 6 | # 7 | # Released under the terms of the MIT License 8 | # (see LICENSE.txt in the project root directory for details) 9 | # ----------------------------------------------------------------------------- 10 | 11 | from __future__ import ( 12 | print_function, division, unicode_literals, absolute_import) 13 | 14 | 15 | import subdir.profiling_test_script2 as script2 16 | 17 | 18 | @profile 19 | def fact(n): 20 | result = 1 21 | for i in range(2, n // 4): 22 | result *= i 23 | result = 1 24 | # This is a comment 25 | for i in range(2, n // 16): 26 | result *= i 27 | result = 1 28 | 29 | if False: 30 | # This won't be run 31 | raise RuntimeError("What are you doing here ???") 32 | 33 | for i in range(2, n + 1): 34 | result *= i 35 | return result 36 | # This is after the end of the function. 37 | 38 | if False: 39 | # This won't be run 40 | raise RuntimeError("It's getting bad.") 41 | 42 | 43 | @profile 44 | def sum_(n): 45 | result = 0 46 | 47 | for i in range(1, n + 1): 48 | result += i 49 | return result 50 | 51 | if __name__ == "__main__": 52 | print(fact(120)) 53 | print(sum_(120)) 54 | print(script2.fact2(120)) 55 | print(script2.sum2(120)) 56 | -------------------------------------------------------------------------------- /spyder_line_profiler/spyder/run_conf.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # Copyright © Spyder Project Contributors 4 | # Licensed under the terms of the MIT License 5 | # (see spyder/__init__.py for details) 6 | 7 | """Line profiler run executor configurations.""" 8 | 9 | # Third-party imports 10 | from qtpy.QtWidgets import ( 11 | QGroupBox, QVBoxLayout, QGridLayout, QCheckBox, QLineEdit) 12 | 13 | # Local imports 14 | from spyder.api.translations import _ 15 | from spyder.plugins.run.api import ( 16 | RunExecutorConfigurationGroup, Context, RunConfigurationMetadata) 17 | 18 | 19 | class LineProfilerConfigurationGroup(RunExecutorConfigurationGroup): 20 | """Run configuration options for line profiler.""" 21 | 22 | def __init__(self, parent, context: Context, input_extension: str, 23 | input_metadata: RunConfigurationMetadata): 24 | super().__init__(parent, context, input_extension, input_metadata) 25 | 26 | self.dir = None 27 | 28 | # --- General settings ---- 29 | common_group = QGroupBox(_("File settings")) 30 | common_layout = QGridLayout(common_group) 31 | 32 | self.clo_cb = QCheckBox(_("Command line options:")) 33 | common_layout.addWidget(self.clo_cb, 0, 0) 34 | self.clo_edit = QLineEdit(self) 35 | self.clo_edit.setMinimumWidth(300) 36 | self.clo_cb.toggled.connect(self.clo_edit.setEnabled) 37 | self.clo_edit.setEnabled(False) 38 | common_layout.addWidget(self.clo_edit, 0, 1) 39 | 40 | layout = QVBoxLayout(self) 41 | layout.setContentsMargins(0, 0, 0, 0) 42 | layout.addWidget(common_group) 43 | layout.addStretch(100) 44 | 45 | @staticmethod 46 | def get_default_configuration() -> dict: 47 | return { 48 | 'args_enabled': False, 49 | 'args': '' 50 | } 51 | 52 | def set_configuration(self, config: dict): 53 | args_enabled = config['args_enabled'] 54 | args = config['args'] 55 | 56 | self.clo_cb.setChecked(args_enabled) 57 | self.clo_edit.setText(args) 58 | 59 | def get_configuration(self) -> dict: 60 | return { 61 | 'args_enabled': self.clo_cb.isChecked(), 62 | 'args': self.clo_edit.text(), 63 | } 64 | -------------------------------------------------------------------------------- /spyder_line_profiler/spyder/confpage.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # ----------------------------------------------------------------------------- 4 | # Copyright (c) 2013- Spyder Project Contributors 5 | # 6 | # Released under the terms of the MIT License 7 | # (see LICENSE.txt in the project root directory for details) 8 | # ----------------------------------------------------------------------------- 9 | 10 | """ 11 | Spyder Line Profiler 5 Preferences Page. 12 | """ 13 | 14 | # Third party imports 15 | from qtpy.QtCore import Qt 16 | from qtpy.QtWidgets import QGroupBox, QLabel, QVBoxLayout 17 | from spyder.api.preferences import PluginConfigPage 18 | from spyder.api.translations import get_translation 19 | 20 | # Local imports 21 | from .widgets import SpyderLineProfilerWidget 22 | 23 | # Localization 24 | _ = get_translation("spyder_line_profiler.spyder") 25 | 26 | 27 | class SpyderLineProfilerConfigPage(PluginConfigPage): 28 | 29 | # --- PluginConfigPage API 30 | # ------------------------------------------------------------------------ 31 | 32 | def setup_page(self): 33 | settings_group = QGroupBox(_("Settings")) 34 | use_color_box = self.create_checkbox( 35 | _("Use deterministic colors to differentiate functions"), 36 | 'use_colors', default=True) 37 | 38 | results_group = QGroupBox(_("Results")) 39 | results_label1 = QLabel(_("Line profiler plugin results " 40 | "(the output of kernprof.py)\n" 41 | "are stored here:")) 42 | results_label1.setWordWrap(True) 43 | 44 | # Warning: do not try to regroup the following QLabel contents with 45 | # widgets above -- this string was isolated here in a single QLabel 46 | # on purpose: to fix Issue 863 of Profiler plugin 47 | results_label2 = QLabel(SpyderLineProfilerWidget.DATAPATH) 48 | 49 | results_label2.setTextInteractionFlags(Qt.TextSelectableByMouse) 50 | results_label2.setWordWrap(True) 51 | 52 | settings_layout = QVBoxLayout() 53 | settings_layout.addWidget(use_color_box) 54 | settings_group.setLayout(settings_layout) 55 | 56 | results_layout = QVBoxLayout() 57 | results_layout.addWidget(results_label1) 58 | results_layout.addWidget(results_label2) 59 | results_group.setLayout(results_layout) 60 | 61 | vlayout = QVBoxLayout() 62 | vlayout.addWidget(settings_group) 63 | vlayout.addWidget(results_group) 64 | vlayout.addStretch(1) 65 | self.setLayout(vlayout) -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to the Spyder line profiler plugin 2 | 3 | :+1::tada: 4 | First off, thanks for taking the time to contribute to the Spyder Line Profiler 5 | plugin! 6 | :tada::+1: 7 | 8 | ## General guidelines for contributing 9 | 10 | The Spyder line profiler plugin is developed as part of the wider Spyder project. 11 | In general, the guidelines for contributing to Spyder also apply here. 12 | Specifically, all contributors are expected to abide by 13 | [Spyder's Code of Conduct](https://github.com/spyder-ide/spyder/blob/master/CODE_OF_CONDUCT.md). 14 | 15 | There are many ways to contribute and all are valued and welcome. 16 | You can help other users, write documentation, spread the word, submit 17 | helpful issues on the 18 | [issue tracker](https://github.com/spyder-ide/spyder-line-profiler/issues) 19 | with problems you encounter or ways to improve the plugin, test the development 20 | version, or submit a pull request on GitHub. 21 | 22 | The rest of this document explains how to set up a development environment. 23 | 24 | ## Setting up a development environment 25 | 26 | This section explains how to set up a conda environment to run and work on the 27 | development version of the Spyder line profiler plugin. 28 | 29 | ### Creating a conda environment 30 | 31 | This creates a new conda environment with the name `spyderlp-dev`. 32 | 33 | ```bash 34 | $ conda create -n spyderlp-dev -c conda-forge python=3.9 35 | $ conda activate spyderlp-dev 36 | ``` 37 | 38 | ### Cloning the repository 39 | 40 | This creates a new directory `spyder-line-profiler` with the source code of the 41 | Spyder line profiler plugin. 42 | 43 | ```bash 44 | $ git clone https://github.com/spyder-ide/spyder-line-profiler.git 45 | $ cd spyder-line-profiler 46 | ``` 47 | 48 | ### Installing dependencies 49 | 50 | This installs Spyder, line_profiler and all other plugin dependencies into 51 | the conda environment previously created, using the conda-forge channel. 52 | 53 | ```bash 54 | $ conda install -c conda-forge --file requirements/conda.txt 55 | ``` 56 | 57 | ### Installing the plugin 58 | 59 | This installs the Spyder line profiler plugin so that Spyder will use it. 60 | 61 | ```bash 62 | $ pip install --no-deps -e . 63 | ``` 64 | 65 | ### Running Spyder 66 | 67 | You are done! You can run Spyder as normal and it should load the line profiler 68 | plugin. 69 | 70 | ```bash 71 | $ spyder 72 | ``` 73 | 74 | ### Running Tests 75 | 76 | This command installs the test dependencies into your conda environment, using the conda-forge channel. 77 | 78 | ```bash 79 | $ conda install -c conda-forge --file requirements/tests.txt 80 | ``` 81 | 82 | You can now run the tests with a simple 83 | 84 | ```bash 85 | $ pytest 86 | ``` 87 | 88 | -------------------------------------------------------------------------------- /RELEASE.md: -------------------------------------------------------------------------------- 1 | # Release 2 | 3 | Follow these steps to release a new version of spyder-line-profiler. 4 | 5 | In the commands below, replace `X.Y.Z` with the release version when needed. 6 | 7 | **Note**: We use `pip` instead of `conda` here even on Conda installs, to ensure we always get the latest upstream versions of the build dependencies. 8 | 9 | ## PyPI and GitHub 10 | 11 | You will need to have a local clone of the repo. The following steps supose a repo setup from a fork with and `upstream` remote pointing to the main `spyder-line-profiler` repo 12 | 13 | * Close [milestone on Github](https://github.com/spyder-ide/spyder-line-profiler/milestones) 14 | 15 | * Update local repo 16 | 17 | ```bash 18 | git restore . && git switch master && git pull upstream master 19 | ``` 20 | 21 | * Clean local repo 22 | 23 | ```bash 24 | git clean -xfdi 25 | ``` 26 | 27 | * Install/upgrade Loghub 28 | 29 | ```bash 30 | pip install --upgrade loghub 31 | ``` 32 | 33 | * Update `CHANGELOG.md` with Loghub: `loghub spyder-ide/spyder-line-profiler --milestone vX.Y.Z` 34 | 35 | * git add and git commit with "Update Changelog" 36 | 37 | * Update `__version__` in `__init__.py` (set release version, remove `dev0`) 38 | 39 | * Create release commit 40 | 41 | ```bash 42 | git commit -am "Release X.Y.Z" 43 | ``` 44 | 45 | * Update the packaging stack 46 | 47 | ```bash 48 | python -m pip install --upgrade pip 49 | pip install --upgrade --upgrade-strategy eager build setuptools twine wheel 50 | ``` 51 | 52 | * Build source distribution and wheel 53 | 54 | ```bash 55 | python -bb -X dev -W error -m build 56 | ``` 57 | 58 | * Check distribution archives 59 | 60 | ```bash 61 | twine check --strict dist/* 62 | ``` 63 | 64 | * Upload distribution packages to PyPI 65 | 66 | ```bash 67 | twine upload dist/* 68 | ``` 69 | 70 | * Create release tag 71 | 72 | ```bash 73 | git tag -a vX.Y.Z -m "Release X.Y.Z" 74 | ``` 75 | 76 | * Update `__version__` in `__init__.py` (add `.dev0` and increment minor) 77 | 78 | * Create `Back to work` commit 79 | 80 | ```bash 81 | git commit -am "Back to work" 82 | ``` 83 | 84 | * Push new release commits and tags to `master` 85 | 86 | ```bash 87 | git push upstream master --follow-tags 88 | ``` 89 | 90 | * Create a [GitHub release](https://github.com/spyder-ide/spyder-line-profiler/releases) from the tag 91 | 92 | ## Conda-Forge 93 | 94 | To release a new version of `spyder-line-profiler` on Conda-Forge: 95 | 96 | * After the release on PyPI, an automatic PR in the [Conda-Forge feedstock repo for spyder-line-profiler](https://github.com/conda-forge/spyder-line-profiler-feedstock/pulls) should open. 97 | Merging this PR will update the respective Conda-Forge package. -------------------------------------------------------------------------------- /spyder_line_profiler/tests/test_lineprofiler.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # ----------------------------------------------------------------------------- 4 | # Copyright (c) 2017- Spyder Project Contributors 5 | # 6 | # Released under the terms of the MIT License 7 | # (see LICENSE.txt in the project root directory for details) 8 | # ----------------------------------------------------------------------------- 9 | 10 | """Tests for lineprofiler.py.""" 11 | 12 | # Standard library imports 13 | import os 14 | import sys 15 | 16 | # Third party imports 17 | from qtpy.QtCore import Qt 18 | from unittest.mock import patch 19 | 20 | # Local imports 21 | from spyder_line_profiler.spyder.widgets import SpyderLineProfilerWidget 22 | 23 | 24 | TEST_SCRIPT = \ 25 | """import time 26 | @profile 27 | def foo(): 28 | time.sleep(1) 29 | xs = [] # Test non-ascii character: Σ 30 | for k in range(100): 31 | xs = xs + ['x'] 32 | foo()""" 33 | 34 | 35 | def test_profile_and_display_results(qtbot, tmpdir): 36 | """Run profiler on simple script and check that results are okay.""" 37 | os.chdir(tmpdir.strpath) 38 | testfilename = tmpdir.join('test_foo.py').strpath 39 | 40 | with open(testfilename, 'w', encoding='utf-8') as f: 41 | f.write(TEST_SCRIPT) 42 | 43 | widget = SpyderLineProfilerWidget(None) 44 | with patch.object(widget, 'get_conf', 45 | return_value=sys.executable) as mock_get_conf, \ 46 | patch('spyder_line_profiler.spyder.widgets.TextEditor') \ 47 | as MockTextEditor: 48 | 49 | widget.setup() 50 | qtbot.addWidget(widget) 51 | with qtbot.waitSignal(widget.sig_finished, timeout=10000, 52 | raising=True): 53 | widget.analyze(testfilename) 54 | 55 | mock_get_conf.assert_called_once_with( 56 | 'executable', section='main_interpreter') 57 | MockTextEditor.assert_not_called() 58 | 59 | dt = widget.datatree 60 | assert dt.topLevelItemCount() == 1 # number of functions profiled 61 | 62 | top = dt.topLevelItem(0) 63 | assert top.data(0, Qt.DisplayRole).startswith('foo ') 64 | assert top.childCount() == 6 65 | for i in range(6): 66 | assert top.child(i).data(0, Qt.DisplayRole) == i + 2 # line no 67 | 68 | assert top.child(2).data(1, Qt.DisplayRole) == '1' # hits 69 | assert top.child(3).data(1, Qt.DisplayRole) == '1' 70 | assert top.child(4).data(1, Qt.DisplayRole) in ['100', '101'] # result depends on Python version 71 | assert top.child(5).data(1, Qt.DisplayRole) == '100' 72 | 73 | assert float(top.child(2).data(2, Qt.DisplayRole)) >= 900 # time (ms) 74 | assert float(top.child(2).data(2, Qt.DisplayRole)) <= 1200 75 | assert float(top.child(3).data(2, Qt.DisplayRole)) <= 100 76 | assert float(top.child(4).data(2, Qt.DisplayRole)) <= 100 77 | assert float(top.child(5).data(2, Qt.DisplayRole)) <= 100 78 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Spyder line profiler plugin 2 | 3 | ## Project details 4 | 5 | ![license](https://img.shields.io/pypi/l/spyder-line-profiler.svg) 6 | [![conda version](https://img.shields.io/conda/v/conda-forge/spyder-line-profiler.svg)](https://www.anaconda.com/download/) 7 | [![download count](https://img.shields.io/conda/d/conda-forge/spyder-line-profiler.svg)](https://www.anaconda.com/download/) 8 | [![pypi version](https://img.shields.io/pypi/v/spyder-line-profiler.svg)](https://pypi.python.org/pypi/spyder-line-profiler) 9 | [![Join the chat at https://gitter.im/spyder-ide/public](https://badges.gitter.im/spyder-ide/spyder.svg)](https://gitter.im/spyder-ide/public) 10 | [![OpenCollective Backers](https://opencollective.com/spyder/backers/badge.svg?color=blue)](#backers) 11 | [![OpenCollective Sponsors](https://opencollective.com/spyder/sponsors/badge.svg?color=blue)](#sponsors) 12 | 13 | ## Build status 14 | 15 | [![Windows status](https://github.com/spyder-ide/spyder-line-profiler/workflows/Windows%20tests/badge.svg)](https://github.com/spyder-ide/spyder-line-profiler/actions?query=workflow%3A%22Windows+tests%22) 16 | [![Linux status](https://github.com/spyder-ide/spyder-line-profiler/workflows/Linux%20tests/badge.svg)](https://github.com/spyder-ide/spyder-line-profiler/actions?query=workflow%3A%22Linux+tests%22) 17 | [![MacOS status](https://github.com/spyder-ide/spyder-line-profiler/workflows/Macos%20tests/badge.svg)](https://github.com/spyder-ide/spyder-line-profiler/actions?query=workflow%3A%22Macos+tests%22) 18 | [![codecov](https://codecov.io/gh/spyder-ide/spyder-line-profiler/branch/master/graph/badge.svg)](https://codecov.io/gh/spyder-ide/spyder-line-profiler/branch/master) 19 | 20 | ## Description 21 | 22 | This is a plugin to run the Python 23 | [line_profiler](https://pypi.python.org/pypi/line_profiler) 24 | from within the Python IDE [Spyder](https://github.com/spyder-ide/spyder). 25 | 26 | The code is an adaptation of the profiler plugin integrated in Spyder. 27 | 28 | ## Installation 29 | 30 | To install this plugin, you can use either ``pip`` or ``conda`` package 31 | managers, as follows: 32 | 33 | Using conda (the recommended way!): 34 | 35 | ``` 36 | conda install spyder-line-profiler -c conda-forge 37 | ``` 38 | 39 | Using pip: 40 | 41 | ``` 42 | pip install spyder-line-profiler 43 | ``` 44 | 45 | ## Usage 46 | 47 | Add a `@profile` decorator to the functions that you wish to profile then 48 | Shift+F10 (line profiler default) to run the profiler on the current script, 49 | or go to `Run > Run line profiler`. 50 | 51 | The results will be shown in a dockwidget, grouped by function. Lines with a 52 | stronger color take more time to run. 53 | 54 | ## Screenshot 55 | 56 | ![Screenshot of spyder-line-profiler plugin showing profiler results](./img_src/screenshot_profiler.png) 57 | 58 | ## Contributing 59 | 60 | Everyone is welcome to contribute! 61 | 62 | ## Sponsors 63 | 64 | Spyder and its subprojects are funded thanks to the generous support of 65 | 66 | [![Quansight](https://static.wixstatic.com/media/095d2c_2508c560e87d436ea00357abc404cf1d~mv2.png/v1/crop/x_0,y_9,w_915,h_329/fill/w_380,h_128,al_c,usm_0.66_1.00_0.01/095d2c_2508c560e87d436ea00357abc404cf1d~mv2.png)](https://www.quansight.com/)[![Numfocus](https://i2.wp.com/numfocus.org/wp-content/uploads/2017/07/NumFocus_LRG.png?fit=320%2C148&ssl=1)](https://numfocus.org/) 67 | 68 | and the donations we have received from our users around the world through [Open Collective](https://opencollective.com/spyder/): 69 | 70 | [![Sponsors](https://opencollective.com/spyder/sponsors.svg)](https://opencollective.com/spyder#support) 71 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # ----------------------------------------------------------------------------- 4 | # Copyright (c) 2015- Spyder Project Contributors 5 | # 6 | # Released under the terms of the MIT License 7 | # (see LICENSE.txt in the project root directory for details) 8 | # ----------------------------------------------------------------------------- 9 | 10 | """ 11 | Setup script for spyder_line_profiler 12 | """ 13 | 14 | from setuptools import setup, find_packages 15 | import os 16 | import os.path as osp 17 | 18 | 19 | def get_version(): 20 | """Get version from source file""" 21 | import codecs 22 | with codecs.open("spyder_line_profiler/__init__.py", encoding="utf-8") as f: 23 | lines = f.read().splitlines() 24 | for l in lines: 25 | if "__version__" in l: 26 | version = l.split("=")[1].strip() 27 | version = version.replace("'", '').replace('"', '') 28 | return version 29 | 30 | 31 | def get_package_data(name, extlist): 32 | """Return data files for package *name* with extensions in *extlist*""" 33 | flist = [] 34 | # Workaround to replace os.path.relpath (not available until Python 2.6): 35 | offset = len(name) + len(os.pathsep) 36 | for dirpath, _dirnames, filenames in os.walk(name): 37 | for fname in filenames: 38 | if not fname.startswith('.') and osp.splitext(fname)[1] in extlist: 39 | flist.append(osp.join(dirpath, fname)[offset:]) 40 | return flist 41 | 42 | 43 | # Requirements 44 | REQUIREMENTS = ['line_profiler', 'qtawesome', 'spyder>=6.1,<6.2'] 45 | EXTLIST = ['.jpg', '.png', '.json', '.mo', '.ini'] 46 | LIBNAME = 'spyder_line_profiler' 47 | 48 | 49 | LONG_DESCRIPTION = """ 50 | This is a plugin for the Spyder IDE that integrates the Python line profiler. 51 | It allows you to see the time spent in every line. 52 | 53 | Usage 54 | ----- 55 | 56 | Add a ``@profile`` decorator to the functions that you wish to profile 57 | then press Shift+F10 (line profiler default) to run the profiler on 58 | the current script, or go to ``Run > Run line profiler``. 59 | 60 | The results will be shown in a dockwidget, grouped by function. Lines 61 | with a stronger color take more time to run. 62 | 63 | .. image: https://raw.githubusercontent.com/spyder-ide/spyder-line-profiler/master/img_src/screenshot_profiler.png 64 | """ 65 | 66 | setup( 67 | name=LIBNAME, 68 | version=get_version(), 69 | packages=find_packages(), 70 | package_data={LIBNAME: get_package_data(LIBNAME, EXTLIST)}, 71 | keywords=["Qt PyQt5 PySide2 spyder plugins spyplugins line_profiler profiler"], 72 | install_requires=REQUIREMENTS, 73 | url='https://github.com/spyder-ide/spyder-line-profiler', 74 | license='MIT', 75 | python_requires='>= 3.10', 76 | entry_points={ 77 | "spyder.plugins": [ 78 | "spyder_line_profiler = spyder_line_profiler.spyder.plugin:SpyderLineProfiler" 79 | ], 80 | }, 81 | author="Spyder Project Contributors", 82 | description='Plugin for the Spyder IDE that integrates the Python line profiler.', 83 | long_description=LONG_DESCRIPTION, 84 | classifiers=[ 85 | 'Development Status :: 4 - Beta', 86 | 'Environment :: X11 Applications :: Qt', 87 | 'Environment :: Win32 (MS Windows)', 88 | 'Intended Audience :: Developers', 89 | 'License :: OSI Approved :: MIT License', 90 | 'Operating System :: OS Independent', 91 | 'Programming Language :: Python :: 3', 92 | 'Programming Language :: Python :: 3.10', 93 | 'Programming Language :: Python :: 3.11', 94 | 'Programming Language :: Python :: 3.12', 95 | 'Topic :: Software Development', 96 | 'Topic :: Text Editors :: Integrated Development Environments (IDE)']) 97 | -------------------------------------------------------------------------------- /.github/workflows/run-tests.yml: -------------------------------------------------------------------------------- 1 | name: Run tests 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | pull_request: 8 | branches: 9 | - master 10 | 11 | jobs: 12 | main: 13 | strategy: 14 | fail-fast: false 15 | matrix: 16 | OS: ['ubuntu', 'macos', 'windows'] 17 | PYTHON_VERSION: ['3.10', '3.11', '3.12'] 18 | SPYDER_SOURCE: ['conda', 'git'] 19 | exclude: 20 | - OS: ['macos', 'windows'] 21 | PYTHON_VERSION: ['3.11'] 22 | name: ${{ matrix.OS }} py${{ matrix.PYTHON_VERSION }} spyder-from-${{ matrix.SPYDER_SOURCE }} 23 | runs-on: ${{ matrix.OS }}-latest 24 | env: 25 | CI: True 26 | PYTHON_VERSION: ${{ matrix.PYTHON_VERSION }} 27 | steps: 28 | - name: Checkout branch 29 | uses: actions/checkout@v4 30 | with: 31 | path: 'spyder-line-profiler' 32 | - name: Install System Packages 33 | if: matrix.OS == 'ubuntu' 34 | run: | 35 | sudo apt-get update --fix-missing 36 | sudo apt-get install -qq pyqt5-dev-tools libxcb-xinerama0 xterm --fix-missing 37 | - name: Install Conda 38 | uses: conda-incubator/setup-miniconda@v3 39 | with: 40 | miniforge-version: latest 41 | auto-update-conda: true 42 | conda-remove-defaults: "true" 43 | python-version: ${{ matrix.PYTHON_VERSION }} 44 | - name: Checkout Spyder from git 45 | if: matrix.SPYDER_SOURCE == 'git' 46 | uses: actions/checkout@v4 47 | with: 48 | repository: 'spyder-ide/spyder' 49 | path: 'spyder' 50 | - name: Install Spyder's dependencies (main) 51 | if: matrix.SPYDER_SOURCE == 'git' 52 | shell: bash -l {0} 53 | run: conda env update --file spyder/requirements/main.yml 54 | - name: Install Spyder's dependencies (Linux) 55 | if: matrix.SPYDER_SOURCE == 'git' && matrix.OS == 'ubuntu' 56 | shell: bash -l {0} 57 | run: conda env update --file spyder/requirements/linux.yml 58 | - name: Install Spyder's dependencies (Mac / Windows) 59 | if: matrix.SPYDER_SOURCE == 'git' && matrix.OS != 'ubuntu' 60 | shell: bash -l {0} 61 | run: conda env update --file spyder/requirements/${{ matrix.OS }}.yml 62 | - name: Install Spyder from source 63 | if: matrix.SPYDER_SOURCE == 'git' 64 | shell: bash -l {0} 65 | run: pip install --no-deps spyder 66 | - name: Install plugin dependencies (without Spyder) 67 | if: matrix.SPYDER_SOURCE == 'git' 68 | shell: bash -l {0} 69 | run: | 70 | python spyder-line-profiler/.github/scripts/generate-without-spyder.py 71 | conda install --file spyder-line-profiler/requirements/without-spyder.txt -y 72 | - name: Install plugin dependencies 73 | if: matrix.SPYDER_SOURCE == 'conda' 74 | shell: bash -l {0} 75 | run: conda install --file spyder-line-profiler/requirements/conda.txt -y 76 | - name: Install test dependencies 77 | shell: bash -l {0} 78 | run: | 79 | conda install nomkl -y -q 80 | conda install --file spyder-line-profiler/requirements/tests.txt -y 81 | - name: Install plugin 82 | shell: bash -l {0} 83 | run: pip install --no-deps spyder-line-profiler 84 | - name: Show environment information 85 | shell: bash -l {0} 86 | run: | 87 | conda info 88 | conda list 89 | - name: Run tests (Linux) 90 | if: matrix.OS == 'ubuntu' 91 | uses: nick-fields/retry@v3 92 | with: 93 | timeout_minutes: 10 94 | max_attempts: 3 95 | shell: bash 96 | command: | 97 | . ~/.profile 98 | xvfb-run --auto-servernum pytest spyder-line-profiler/spyder_line_profiler -vv 99 | - name: Run tests (MacOS) 100 | if: matrix.OS == 'macos' 101 | uses: nick-fields/retry@v3 102 | with: 103 | timeout_minutes: 10 104 | max_attempts: 3 105 | shell: bash 106 | command: | 107 | . ~/.profile 108 | pytest spyder-line-profiler/spyder_line_profiler -x -vv 109 | - name: Run tests (Windows) 110 | if: matrix.OS == 'windows' 111 | uses: nick-fields/retry@v3 112 | with: 113 | timeout_minutes: 10 114 | max_attempts: 3 115 | command: pytest spyder-line-profiler/spyder_line_profiler -x -vv 116 | -------------------------------------------------------------------------------- /spyder_line_profiler/spyder/plugin.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # ----------------------------------------------------------------------------- 4 | # Copyright (c) 2013- Spyder Project Contributors 5 | # 6 | # Released under the terms of the MIT License 7 | # (see LICENSE.txt in the project root directory for details) 8 | # ----------------------------------------------------------------------------- 9 | 10 | """ 11 | Spyder Line Profiler Plugin. 12 | """ 13 | 14 | # Third-party imports 15 | import qtawesome as qta 16 | from qtpy.QtCore import Qt, Signal 17 | 18 | # Spyder imports 19 | from spyder.api.plugins import Plugins, SpyderDockablePlugin 20 | from spyder.api.translations import get_translation 21 | from spyder.api.plugin_registration.decorators import ( 22 | on_plugin_available, on_plugin_teardown) 23 | from spyder.plugins.mainmenu.api import ApplicationMenus, RunMenuSections 24 | from spyder.plugins.run.api import RunContext, RunExecutor, run_execute 25 | from spyder.utils.icon_manager import ima 26 | 27 | # Local imports 28 | from spyder_line_profiler.spyder.config import ( 29 | CONF_SECTION, CONF_DEFAULTS, CONF_VERSION) 30 | from spyder_line_profiler.spyder.confpage import SpyderLineProfilerConfigPage 31 | from spyder_line_profiler.spyder.widgets import ( 32 | SpyderLineProfilerWidget, is_lineprofiler_installed) 33 | from spyder_line_profiler.spyder.run_conf import LineProfilerConfigurationGroup 34 | 35 | # Localization 36 | _ = get_translation("spyder_line_profiler.spyder") 37 | 38 | 39 | class SpyderLineProfiler(SpyderDockablePlugin, RunExecutor): 40 | """ 41 | Spyder Line Profiler plugin for Spyder 5. 42 | """ 43 | 44 | NAME = "spyder_line_profiler" 45 | REQUIRES = [Plugins.Preferences, Plugins.Editor, Plugins.Run] 46 | OPTIONAL = [] 47 | TABIFY = [Plugins.Help] 48 | WIDGET_CLASS = SpyderLineProfilerWidget 49 | CONF_SECTION = CONF_SECTION 50 | CONF_DEFAULTS = CONF_DEFAULTS 51 | CONF_VERSION = CONF_VERSION 52 | CONF_WIDGET_CLASS = SpyderLineProfilerConfigPage 53 | CONF_FILE = True 54 | 55 | # --- Signals 56 | sig_finished = Signal() 57 | """This signal is emitted to inform the profile profiling has finished.""" 58 | 59 | # --- SpyderDockablePlugin API 60 | # ------------------------------------------------------------------------ 61 | @staticmethod 62 | def get_name(): 63 | return _("Line Profiler") 64 | 65 | @staticmethod 66 | def get_description(): 67 | return _("Line profiler display for Spyder") 68 | 69 | @classmethod 70 | def get_icon(cls): 71 | return qta.icon('mdi.speedometer', color=ima.MAIN_FG_COLOR) 72 | 73 | def on_initialize(self): 74 | self.widget = self.get_widget() 75 | self.widget.sig_finished.connect(self.sig_finished) 76 | 77 | self.executor_configuration = [ 78 | { 79 | 'input_extension': 'py', 80 | 'context': { 81 | 'name': 'File' 82 | }, 83 | 'output_formats': [], 84 | 'configuration_widget': LineProfilerConfigurationGroup, 85 | 'requires_cwd': True, 86 | 'priority': 7 87 | } 88 | ] 89 | 90 | @on_plugin_available(plugin=Plugins.Run) 91 | def on_run_available(self): 92 | run = self.get_plugin(Plugins.Run) 93 | run.register_executor_configuration(self, self.executor_configuration) 94 | 95 | if is_lineprofiler_installed(): 96 | run.create_run_in_executor_button( 97 | RunContext.File, 98 | self.NAME, 99 | text=_('Run line profiler'), 100 | tip=_('Run line profiler'), 101 | icon=self.get_icon(), 102 | shortcut_context='spyder_line_profiler', 103 | register_shortcut=True, 104 | add_to_menu={ 105 | "menu": ApplicationMenus.Run, 106 | "section": RunMenuSections.RunInExecutors 107 | }, 108 | shortcut_widget_context=Qt.ApplicationShortcut, 109 | ) 110 | @on_plugin_available(plugin=Plugins.Editor) 111 | def on_editor_available(self): 112 | widget = self.get_widget() 113 | editor = self.get_plugin(Plugins.Editor) 114 | widget.sig_edit_goto_requested.connect(editor.load) 115 | 116 | @on_plugin_available(plugin=Plugins.Preferences) 117 | def on_preferences_available(self): 118 | preferences = self.get_plugin(Plugins.Preferences) 119 | preferences.register_plugin_preferences(self) 120 | 121 | @on_plugin_teardown(plugin=Plugins.Run) 122 | def on_run_teardown(self): 123 | run = self.get_plugin(Plugins.Run) 124 | run.deregister_executor_configuration( 125 | self, self.executor_configuration) 126 | run.destroy_run_in_executor_button( 127 | RunContext.File, self.NAME) 128 | 129 | @on_plugin_teardown(plugin=Plugins.Preferences) 130 | def on_preferences_teardown(self): 131 | preferences = self.get_plugin(Plugins.Preferences) 132 | preferences.deregister_plugin_preferences(self) 133 | 134 | @on_plugin_teardown(plugin=Plugins.Editor) 135 | def on_editor_teardown(self): 136 | widget = self.get_widget() 137 | editor = self.get_plugin(Plugins.Editor) 138 | widget.sig_edit_goto_requested.disconnect(editor.load) 139 | 140 | def check_compatibility(self): 141 | valid = True 142 | message = "" # Note: Remember to use _("") to localize the string 143 | return valid, message 144 | 145 | def on_close(self, cancellable=True): 146 | return True 147 | 148 | # --- Public API 149 | # ------------------------------------------------------------------------ 150 | 151 | @run_execute(context=RunContext.File) 152 | def run_file(self, input, conf): 153 | self.switch_to_plugin() 154 | 155 | exec_params = conf['params'] 156 | cwd_opts = exec_params['working_dir'] 157 | params = exec_params['executor_params'] 158 | 159 | run_input = input['run_input'] 160 | filename = run_input['path'] 161 | 162 | wdir = cwd_opts['path'] 163 | args = params['args'] 164 | 165 | self.get_widget().analyze(filename, wdir=wdir, args=args) 166 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # History of changes 2 | 3 | ## Version 0.4.2 (2025/10/11) 4 | 5 | This version fixes a bug and makes the plugin compatible with Spyder 6.1. 6 | 7 | ### Bug fix 8 | 9 | * Fix shortcut to run profiler ([PR 100](https://github.com/spyder-ide/spyder-line-profiler/pull/100) by [@rear1019](https://github.com/rear1019)) 10 | 11 | ### Maintenance 12 | 13 | * Define run configuration options to make plugin compatible with Spyder 6.1 ([Issue 101](https://github.com/spyder-ide/spyder-line-profiler/issues/101), [Issue 104](https://github.com/spyder-ide/spyder-line-profiler/issues/104), ([PR 105](https://github.com/spyder-ide/spyder-line-profiler/pull/105)) 14 | * Drop Python 3.8 and Python 3.9 ([PR 102](https://github.com/spyder-ide/spyder-line-profiler/pull/102)) 15 | 16 | 17 | ## Version 0.4.1 (2025/03/10) 18 | 19 | This release contains some bug fixes. Thanks to [@rear1019](https://github.com/rear1019) who contributed all the changes except for the last PR. 20 | 21 | ### Bug fixes 22 | 23 | * Fix go-to-line when clicking entries ([PR 94](https://github.com/spyder-ide/spyder-line-profiler/pull/94)) 24 | * Fix setting of working directory ([Issue 85](https://github.com/spyder-ide/spyder-line-profiler/issues/85), [PR 96](https://github.com/spyder-ide/spyder-line-profiler/pull/96)) 25 | * Consistently use UTF-8 encoding even if the system default is different ([PR 99](https://github.com/spyder-ide/spyder-line-profiler/pull/99)) 26 | * Fix runtime dependency check ([PR 95](https://github.com/spyder-ide/spyder-line-profiler/pull/95)) 27 | 28 | ### Maintenance 29 | 30 | * Remove QTextCodec for better compatibility with Qt 6 ([Issue 98](https://github.com/spyder-ide/spyder-line-profiler/issues/98), [PR 99](https://github.com/spyder-ide/spyder-line-profiler/pull/99)) 31 | * Fix GitHub automatic testing ([PR 97](https://github.com/spyder-ide/spyder-line-profiler/pull/97)) 32 | 33 | 34 | ## Version 0.4.0 (2024/09/03) 35 | 36 | This release updates the plugin to be used with Spyder 6 and fixes a bug. 37 | 38 | ### Bug fix 39 | 40 | * Allow Python code to have non-ASCII characters ([Issue 90](https://github.com/spyder-ide/spyder-line-profiler/issues/90), [PR 92](https://github.com/spyder-ide/spyder-line-profiler/pull/92)) 41 | 42 | ### Maintenance 43 | 44 | * Make plugin compatible with Spyder 6 ([Issue 86](https://github.com/spyder-ide/spyder-line-profiler/issues/86), [Issue 91](https://github.com/spyder-ide/spyder-line-profiler/issues/91), [PR 87](https://github.com/spyder-ide/spyder-line-profiler/pull/87), [PR 93](https://github.com/spyder-ide/spyder-line-profiler/pull/93)) 45 | * Thanks to [Reinert Huseby Karlsen](https://github.com/rhkarls) and [Simon Kern](https://github.com/skjerns) for help with this! 46 | 47 | 48 | ## Version 0.3.2 (2023/06/24) 49 | 50 | This version contains some bug fixes and is compatible with Spyder 5.4. 51 | 52 | ### Bug fixes 53 | 54 | * Use Python interpreter/environment from Preferences ([Issue 67](https://github.com/spyder-ide/spyder-line-profiler/issues/67), [Issue 5](https://github.com/spyder-ide/spyder-line-profiler/issues/5), [PR 78](https://github.com/spyder-ide/spyder-line-profiler/pull/78)) 55 | * Adapt colors to Spyder's palette ([Issue 50](https://github.com/spyder-ide/spyder-line-profiler/issues/50), [PR 82](https://github.com/spyder-ide/spyder-line-profiler/pull/82)) 56 | * Update LICENSE.txt to match individual file copyright statements ([Issue 74](https://github.com/spyder-ide/spyder-line-profiler/issues/74), [PR 79](https://github.com/spyder-ide/spyder-line-profiler/pull/79)) 57 | * Update description on PyPI ([Issue 73](https://github.com/spyder-ide/spyder-line-profiler/issues/73), [PR 83](https://github.com/spyder-ide/spyder-line-profiler/pull/83)) 58 | 59 | ### Maintenance 60 | 61 | * Updates for Spyder 5.4 ([Issue 80](https://github.com/spyder-ide/spyder-line-profiler/issues/80), [Issue 72](https://github.com/spyder-ide/spyder-line-profiler/issues/72), [PR 77](https://github.com/spyder-ide/spyder-line-profiler/pull/77), [PR 84](https://github.com/spyder-ide/spyder-line-profiler/pull/84)) 62 | * Remove last bits of Python 2 support ([PR 68](https://github.com/spyder-ide/spyder-line-profiler/pull/68)) 63 | * Update test for line_profiler 4.x ([Issue 75](https://github.com/spyder-ide/spyder-line-profiler/issues/75), [PR 68](https://github.com/spyder-ide/spyder-line-profiler/pull/68)) 64 | * Update GitHub test action ([PR 76](https://github.com/spyder-ide/spyder-line-profiler/pull/76)) 65 | 66 | 67 | ## Version 0.3.1 (2022/08/07) 68 | 69 | This version fixes a compatibility issue with Spyder 5.3.2 ([Issue 65](https://github.com/spyder-ide/spyder-line-profiler/issues/65), [PR 66](https://github.com/spyder-ide/spyder-line-profiler/pull/66)). 70 | 71 | 72 | ## Version 0.3.0 (2022/06/03) 73 | 74 | This version is compatible with Spyder 5.2 and 5.3. 75 | 76 | ### Issues Closed 77 | 78 | * [Issue 54](https://github.com/spyder-ide/spyder-line-profiler/issues/54) - How to proceed with spyder 5 compatibility ([PR 56](https://github.com/spyder-ide/spyder-line-profiler/pull/56) by [@skjerns](https://github.com/skjerns)) 79 | * [Issue 52](https://github.com/spyder-ide/spyder-line-profiler/issues/52) - Spyder 5 compatibility ([PR 56](https://github.com/spyder-ide/spyder-line-profiler/pull/56) by [@skjerns](https://github.com/skjerns)) 80 | * [Issue 48](https://github.com/spyder-ide/spyder-line-profiler/issues/48) - Correctly register shortcuts 81 | * [Issue 27](https://github.com/spyder-ide/spyder-line-profiler/issues/27) - saving profiling results 82 | * [Issue 25](https://github.com/spyder-ide/spyder-line-profiler/issues/25) - Text box for file to be profiled accept directories 83 | 84 | In this release 5 issues were closed. 85 | 86 | ### Pull Requests Merged 87 | 88 | * [PR 62](https://github.com/spyder-ide/spyder-line-profiler/pull/62) - PR: Update `README.md`, `CONTRIBUTING.md`, screenshot and add `RELEASE.md`, by [@dalthviz](https://github.com/dalthviz) 89 | * [PR 61](https://github.com/spyder-ide/spyder-line-profiler/pull/61) - PR: Add default config and change plugin icon, by [@dalthviz](https://github.com/dalthviz) 90 | * [PR 60](https://github.com/spyder-ide/spyder-line-profiler/pull/60) - PR: Remove outdated `conda.recipe` directory, by [@dalthviz](https://github.com/dalthviz) 91 | * [PR 56](https://github.com/spyder-ide/spyder-line-profiler/pull/56) - PR: Switch to new API for Spyder 5, by [@skjerns](https://github.com/skjerns) ([54](https://github.com/spyder-ide/spyder-line-profiler/issues/54), [53](https://github.com/spyder-ide/spyder-line-profiler/issues/53), [52](https://github.com/spyder-ide/spyder-line-profiler/issues/52)) 92 | 93 | In this release 4 pull requests were closed. 94 | 95 | 96 | ## Version 0.2.1 (2020/04/28) 97 | 98 | This release fixes some compatibility issues with Spyder 4.1 and some other bugs. 99 | 100 | ### Issues Closed 101 | 102 | * [Issue 44](https://github.com/spyder-ide/spyder-line-profiler/issues/44) - TextEditor initializer receives unexpected argument size ([PR 46](https://github.com/spyder-ide/spyder-line-profiler/pull/46)) 103 | * [Issue 41](https://github.com/spyder-ide/spyder-line-profiler/issues/41) - Move CI to github actions ([PR 45](https://github.com/spyder-ide/spyder-line-profiler/pull/45)) 104 | * [Issue 39](https://github.com/spyder-ide/spyder-line-profiler/issues/39) - Crash from opening options ([PR 40](https://github.com/spyder-ide/spyder-line-profiler/pull/40)) 105 | * [Issue 35](https://github.com/spyder-ide/spyder-line-profiler/issues/35) - Opening editor from line profiler output is broken ([PR 47](https://github.com/spyder-ide/spyder-line-profiler/pull/47)) 106 | 107 | In this release 4 issues were closed. 108 | 109 | ### Pull Requests Merged 110 | 111 | * [PR 47](https://github.com/spyder-ide/spyder-line-profiler/pull/47) - PR: Fix opening editor from profiler widget ([35](https://github.com/spyder-ide/spyder-line-profiler/issues/35)) 112 | * [PR 46](https://github.com/spyder-ide/spyder-line-profiler/pull/46) - PR: Fix initialization of TextEditor ([44](https://github.com/spyder-ide/spyder-line-profiler/issues/44)) 113 | * [PR 45](https://github.com/spyder-ide/spyder-line-profiler/pull/45) - PR: Move CI to GitHub Actions ([41](https://github.com/spyder-ide/spyder-line-profiler/issues/41)) 114 | * [PR 43](https://github.com/spyder-ide/spyder-line-profiler/pull/43) - PR: Fix invalid escape sequence in regex string 115 | * [PR 40](https://github.com/spyder-ide/spyder-line-profiler/pull/40) - PR: Add CONF_DEFAULTS ([39](https://github.com/spyder-ide/spyder-line-profiler/issues/39)) 116 | 117 | In this release 5 pull requests were closed. 118 | 119 | 120 | ## Version 0.2.0 (2019/12/18) 121 | 122 | This release updates the plugin to be used with Spyder 4 and fixes some bugs. 123 | 124 | ### Issues Closed 125 | 126 | * [Issue 33](https://github.com/spyder-ide/spyder-line-profiler/issues/33) - Sorting by time / % not working correctly ([PR 38](https://github.com/spyder-ide/spyder-line-profiler/pull/38)) 127 | * [Issue 26](https://github.com/spyder-ide/spyder-line-profiler/issues/26) - Update plugin to Spyder v4 ([PR 36](https://github.com/spyder-ide/spyder-line-profiler/pull/36)) 128 | 129 | In this release 2 issues were closed. 130 | 131 | ### Pull Requests Merged 132 | 133 | * [PR 38](https://github.com/spyder-ide/spyder-line-profiler/pull/38) - PR: Add natural sort for columns ([33](https://github.com/spyder-ide/spyder-line-profiler/issues/33)) 134 | * [PR 36](https://github.com/spyder-ide/spyder-line-profiler/pull/36) - PR: Compatibility changes for Spyder 4 ([26](https://github.com/spyder-ide/spyder-line-profiler/issues/26)) 135 | * [PR 31](https://github.com/spyder-ide/spyder-line-profiler/pull/31) - PR: Fix continuous integration services 136 | * [PR 30](https://github.com/spyder-ide/spyder-line-profiler/pull/30) - PR: "Profile by line" Button Behavior 137 | * [PR 24](https://github.com/spyder-ide/spyder-line-profiler/pull/24) - Update readme: Plugin can now be installed using conda or pip 138 | * [PR 23](https://github.com/spyder-ide/spyder-line-profiler/pull/23) - Add conda recipe ([15](https://github.com/spyder-ide/spyder-line-profiler/issues/15)) 139 | 140 | In this release 6 pull requests were closed. 141 | 142 | 143 | ## Version 0.1.1 (2017/03/26) 144 | 145 | This version improves the packaging. The code itself was not changed. 146 | 147 | ### Pull Requests Merged 148 | 149 | * [PR 22](https://github.com/spyder-ide/spyder-line-profiler/pull/22) - Install tests alongside package 150 | 151 | In this release 1 pull request was closed. 152 | 153 | 154 | ## Version 0.1.0 (2017/03/22) 155 | 156 | Initial release. 157 | -------------------------------------------------------------------------------- /spyder_line_profiler/spyder/widgets.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # ----------------------------------------------------------------------------- 4 | # Copyright (c) 2013- Spyder Project Contributors 5 | # 6 | # Released under the terms of the MIT License 7 | # (see LICENSE.txt in the project root directory for details) 8 | # ----------------------------------------------------------------------------- 9 | """ 10 | Spyder Line Profiler Main Widget. 11 | """ 12 | # Standard library imports 13 | import inspect 14 | import linecache 15 | import logging 16 | import os 17 | import os.path as osp 18 | import pickle 19 | import re 20 | import time 21 | from datetime import datetime 22 | 23 | # Third party imports 24 | from qtpy.QtGui import QBrush, QColor, QFont 25 | from qtpy.QtCore import (QByteArray, QProcess, Qt, 26 | QProcessEnvironment, Signal, QTimer) 27 | from qtpy.QtWidgets import (QMessageBox, QVBoxLayout, QLabel, 28 | QTreeWidget, QTreeWidgetItem, QApplication) 29 | from qtpy.compat import getopenfilename, getsavefilename 30 | 31 | # Spyder imports 32 | from spyder.api.config.decorators import on_conf_change 33 | from spyder.api.translations import get_translation 34 | from spyder.api.widgets.main_widget import PluginMainWidget 35 | from spyder.config.base import get_conf_path 36 | from spyder.plugins.variableexplorer.widgets.texteditor import TextEditor 37 | from spyder.utils import programs 38 | from spyder.utils.misc import getcwd_or_home 39 | from spyder.utils.palette import SpyderPalette 40 | from spyder.widgets.comboboxes import PythonModulesComboBox 41 | 42 | # Local imports 43 | from spyder_line_profiler.spyder.config import CONF_SECTION 44 | 45 | # Localization and logging 46 | _ = get_translation("spyder") 47 | logger = logging.getLogger(__name__) 48 | 49 | COL_NO = 0 50 | COL_HITS = 1 51 | COL_TIME = 2 52 | COL_PERHIT = 3 53 | COL_PERCENT = 4 54 | COL_LINE = 5 55 | COL_POS = 0 # Position is not displayed but set as Qt.UserRole 56 | 57 | CODE_NOT_RUN_COLOR = QBrush(QColor.fromRgb(128, 128, 128, 200)) 58 | 59 | # Cycle to use when coloring lines from different functions 60 | COLOR_CYCLE = [ 61 | SpyderPalette.GROUP_1, 62 | SpyderPalette.GROUP_4, 63 | SpyderPalette.GROUP_10, 64 | SpyderPalette.GROUP_12, 65 | SpyderPalette.GROUP_2, 66 | SpyderPalette.GROUP_8, 67 | SpyderPalette.GROUP_6] 68 | 69 | WEBSITE_URL = 'http://pythonhosted.org/line_profiler/' 70 | 71 | 72 | def is_lineprofiler_installed(): 73 | """ 74 | Check if the program and the library for line_profiler is installed. 75 | """ 76 | return (programs.is_module_installed('line_profiler') 77 | and programs.is_module_installed('kernprof')) 78 | 79 | 80 | class TreeWidgetItem(QTreeWidgetItem): 81 | """ 82 | An extension of QTreeWidgetItem that replaces the sorting behaviour 83 | such that the sorting is not purely by ASCII index but by natural 84 | sorting, e.g. multi-digit numbers sorted based on their value instead 85 | of individual digits. 86 | 87 | Taken from 88 | https://stackoverflow.com/questions/21030719/sort-a-pyside-qtgui- 89 | qtreewidget-by-an-alpha-numeric-column/ 90 | """ 91 | def __lt__(self, other): 92 | """ 93 | Compare a widget text entry to another entry. 94 | """ 95 | column = self.treeWidget().sortColumn() 96 | key1 = self.text(column) 97 | key2 = other.text(column) 98 | return self.natural_sort_key(key1) < self.natural_sort_key(key2) 99 | 100 | @staticmethod 101 | def natural_sort_key(key): 102 | """ 103 | Natural sorting for both numbers and strings containing numbers. 104 | """ 105 | regex = r'(\d*\.\d+|\d+)' 106 | parts = re.split(regex, key) 107 | return tuple((e if i % 2 == 0 else float(e)) 108 | for i, e in enumerate(parts)) 109 | 110 | 111 | class SpyderLineProfilerWidgetActions: 112 | # Triggers 113 | Browse = 'browse_action' 114 | Clear = 'clear_action' 115 | Collapse = 'collapse_action' 116 | Expand = 'expand_action' 117 | LoadData = 'load_data_action' 118 | Run = 'run_action' 119 | SaveData = 'save_data_action' 120 | ShowOutput = 'show_output_action' 121 | Stop = 'stop_action' 122 | 123 | 124 | class SpyderLineProfilerWidgetMainToolbarSections: 125 | Main = 'main_section' 126 | ExpandCollaps = 'expand_collaps_section' 127 | ShowOutput = 'show_output_section' 128 | 129 | 130 | class SpyderLineProfilerWidgetToolbars: 131 | Information = 'information_toolbar' 132 | 133 | 134 | class SpyderLineProfilerWidgetMainToolbarItems: 135 | FileCombo = 'file_combo' 136 | 137 | 138 | class SpyderLineProfilerWidgetInformationToolbarSections: 139 | Main = 'main_section' 140 | 141 | 142 | class SpyderLineProfilerWidgetInformationToolbarItems: 143 | Stretcher1 = 'stretcher_1' 144 | Stretcher2 = 'stretcher_2' 145 | DateLabel = 'date_label' 146 | 147 | 148 | class SpyderLineProfilerWidget(PluginMainWidget): 149 | 150 | # PluginMainWidget class constants 151 | CONF_SECTION = CONF_SECTION 152 | DATAPATH = get_conf_path('lineprofiler.results') 153 | VERSION = '0.0.1' 154 | 155 | redirect_stdio = Signal(bool) 156 | sig_finished = Signal() 157 | # Signals 158 | sig_edit_goto_requested = Signal(str, int, str) 159 | """ 160 | This signal will request to open a file in a given row and column 161 | using a code editor. 162 | 163 | Parameters 164 | ---------- 165 | path: str 166 | Path to file. 167 | row: int 168 | Cursor starting row position. 169 | word: str 170 | Word to select on given row. 171 | """ 172 | 173 | def __init__(self, name=None, plugin=None, parent=None): 174 | super().__init__(name, plugin, parent) 175 | self.setWindowTitle("Line profiler") 176 | 177 | # Attributes 178 | self._last_wdir = None 179 | self._last_args = None 180 | self.pythonpath = None 181 | self.error_output = None 182 | self.output = None 183 | self.use_colors = True 184 | self.process = None 185 | self.started_time = None 186 | 187 | # Widgets 188 | self.filecombo = PythonModulesComboBox( 189 | self, id_=SpyderLineProfilerWidgetMainToolbarItems.FileCombo) 190 | self.datatree = LineProfilerDataTree(self) 191 | self.datelabel = QLabel(self) 192 | self.datelabel.ID = SpyderLineProfilerWidgetInformationToolbarItems.DateLabel 193 | self.datelabel.setText(_('Please select a file to profile, with ' 194 | 'added @profile decorators for functions')) 195 | self.timer = QTimer(self) 196 | 197 | layout = QVBoxLayout() 198 | layout.addWidget(self.datatree) 199 | self.setLayout(layout) 200 | 201 | # Signals 202 | self.datatree.sig_edit_goto_requested.connect( 203 | self.sig_edit_goto_requested) 204 | 205 | # --- PluginMainWidget API 206 | # ------------------------------------------------------------------------ 207 | def get_title(self): 208 | return _("Line Profiler") 209 | 210 | def get_focus_widget(self): 211 | pass 212 | 213 | def setup(self): 214 | 215 | self.start_action = self.create_action( 216 | SpyderLineProfilerWidgetActions.Run, 217 | text=_("Profile by line"), 218 | tip=_("Run line profiler"), 219 | icon=self.create_icon('run'), 220 | triggered=self.start, 221 | ) 222 | self.stop_action = self.create_action( 223 | SpyderLineProfilerWidgetActions.Stop, 224 | text=_("Stop"), 225 | tip=_("Stop current profiling"), 226 | icon=self.create_icon('stop'), 227 | triggered=self.kill_if_running, 228 | ) 229 | self.browse_action = self.create_action( 230 | SpyderLineProfilerWidgetActions.Browse, 231 | text=_("Open Script"), 232 | tip=_('Select Python script'), 233 | icon=self.create_icon('fileopen'), 234 | triggered=self.select_file, 235 | ) 236 | self.log_action = self.create_action( 237 | SpyderLineProfilerWidgetActions.ShowOutput, 238 | text=_("Show Result"), 239 | tip=_("Show program's output"), 240 | icon=self.create_icon('log'), 241 | triggered=self.show_log, 242 | ) 243 | self.collapse_action = self.create_action( 244 | SpyderLineProfilerWidgetActions.Collapse, 245 | text=_("Collaps"), 246 | tip=_('Collapse all'), 247 | icon=self.create_icon('collapse'), 248 | triggered=lambda dD=-1: self.datatree.collapseAll(), 249 | ) 250 | self.expand_action = self.create_action( 251 | SpyderLineProfilerWidgetActions.Expand, 252 | text=_("Expand"), 253 | tip=_('Expand all'), 254 | icon=self.create_icon('expand'), 255 | triggered=lambda dD=-1: self.datatree.expandAll(), 256 | ) 257 | self.save_action = self.create_action( 258 | SpyderLineProfilerWidgetActions.SaveData, 259 | text=_("Save data"), 260 | tip=_('Save line profiling data'), 261 | icon=self.create_icon('filesave'), 262 | triggered=self.save_data, 263 | ) 264 | self.clear_action = self.create_action( 265 | SpyderLineProfilerWidgetActions.Clear, 266 | text=_("Clear output"), 267 | tip=_('Clear'), 268 | icon=self.create_icon('editdelete'), 269 | triggered=self.clear_data, 270 | ) 271 | 272 | self.set_running_state(False) 273 | self.start_action.setEnabled(False) 274 | self.clear_action.setEnabled(False) 275 | self.log_action.setEnabled(False) 276 | self.save_action.setEnabled(False) 277 | 278 | # Main Toolbar 279 | toolbar = self.get_main_toolbar() 280 | for item in [self.filecombo, self.browse_action, self.start_action, 281 | self.stop_action]: 282 | self.add_item_to_toolbar( 283 | item, 284 | toolbar=toolbar, 285 | section=SpyderLineProfilerWidgetMainToolbarSections.Main, 286 | ) 287 | 288 | # Secondary Toolbar 289 | secondary_toolbar = self.create_toolbar( 290 | SpyderLineProfilerWidgetToolbars.Information) 291 | for item in [self.collapse_action, self.expand_action, 292 | self.create_stretcher( 293 | id_=SpyderLineProfilerWidgetInformationToolbarItems.Stretcher1), 294 | self.datelabel, 295 | self.create_stretcher( 296 | id_=SpyderLineProfilerWidgetInformationToolbarItems.Stretcher2), 297 | self.log_action, 298 | self.save_action, 299 | self.clear_action]: 300 | self.add_item_to_toolbar( 301 | item, 302 | toolbar=secondary_toolbar, 303 | section=SpyderLineProfilerWidgetInformationToolbarSections.Main, 304 | ) 305 | 306 | if not is_lineprofiler_installed(): 307 | for widget in (self.datatree, self.filecombo, self.log_action, 308 | self.start_action, self.stop_action, self.browse_action, 309 | self.collapse_action, self.expand_action): 310 | widget.setDisabled(True) 311 | text = _( 312 | 'Please install the line_profiler module' 313 | ) % WEBSITE_URL 314 | self.datelabel.setText(text) 315 | self.datelabel.setOpenExternalLinks(True) 316 | else: 317 | pass 318 | 319 | def analyze(self, filename=None, wdir=None, args=None, use_colors=True): 320 | self.use_colors = use_colors 321 | if not is_lineprofiler_installed(): 322 | return 323 | self.kill_if_running() 324 | #index, _data = self.get_data(filename) # FIXME: storing data is not implemented yet 325 | if filename is not None: 326 | filename = osp.abspath(str(filename)) 327 | index = self.filecombo.findText(filename) 328 | if index == -1: 329 | self.filecombo.addItem(filename) 330 | self.filecombo.setCurrentIndex(self.filecombo.count()-1) 331 | else: 332 | self.filecombo.setCurrentIndex(index) 333 | self.filecombo.selected() 334 | 335 | if self.filecombo.is_valid(): 336 | filename = str(self.filecombo.currentText()) 337 | if wdir is None: 338 | wdir = osp.dirname(filename) 339 | self.start(wdir, args) 340 | 341 | def select_file(self): 342 | self.redirect_stdio.emit(False) 343 | pwd = getcwd_or_home() 344 | 345 | filename, _selfilter = getopenfilename( 346 | self, _("Select Python script"), pwd, 347 | _("Python scripts")+" (*.py ; *.pyw)") 348 | self.redirect_stdio.emit(False) 349 | 350 | if filename: 351 | self.analyze(filename) 352 | 353 | def show_log(self): 354 | if self.output: 355 | editor = TextEditor(self.output, title=_("Line profiler output"), 356 | readonly=True, parent=self) 357 | 358 | # Call .show() to dynamically resize editor; 359 | # see spyder-ide/spyder#12202 360 | editor.show() 361 | editor.exec_() 362 | 363 | def show_errorlog(self): 364 | if self.error_output: 365 | editor = TextEditor(self.error_output, 366 | title=_("Line profiler output"), 367 | readonly=True, parent=self) 368 | self.datelabel.setText(_('Profiling did not complete (error)')) 369 | # Call .show() to dynamically resize editor; 370 | # see spyder-ide/spyder#12202 371 | editor.show() 372 | editor.exec_() 373 | 374 | def update_timer(self): 375 | elapsed = str(datetime.now() - self.started_time).split(".")[0] 376 | self.datelabel.setText(_(f'Profiling, please wait... elapsed: {elapsed}')) 377 | 378 | def start(self, wdir=None, args=None): 379 | filename = str(self.filecombo.currentText()) 380 | 381 | if wdir in [None, False]: 382 | wdir = self._last_wdir 383 | if wdir in [None, False]: 384 | wdir = osp.dirname(filename) 385 | 386 | if args is None: 387 | args = self._last_args 388 | if args is None: 389 | args = [] 390 | 391 | self._last_wdir = wdir 392 | self._last_args = args 393 | 394 | self.datelabel.setText(_('Profiling starting up, please wait...')) 395 | self.started_time = datetime.now() 396 | 397 | self.process = QProcess(self) 398 | self.process.setProcessChannelMode(QProcess.SeparateChannels) 399 | self.process.setWorkingDirectory(wdir) 400 | self.process.readyReadStandardOutput.connect(self.read_output) 401 | self.process.readyReadStandardError.connect( 402 | lambda: self.read_output(error=True)) 403 | self.process.finished.connect(self.finished) 404 | 405 | proc_env = QProcessEnvironment() 406 | for k, v in os.environ.items(): 407 | proc_env.insert(k, v) 408 | proc_env.remove('PYTHONPATH') 409 | if self.pythonpath is not None: 410 | logger.debug(f"Pass Pythonpath {self.pythonpath} to process") 411 | proc_env.insert('PYTHONPATH', os.pathsep.join(self.pythonpath)) 412 | self.process.setProcessEnvironment(proc_env) 413 | 414 | self.clear_data() 415 | self.error_output = '' 416 | 417 | # Use UTF-8 mode so that profiler writes its output to DATAPATH using 418 | # UTF-8 encoding, instead of the ANSI code page on Windows. 419 | # See issue spyder-ide/spyder-line-profiler#90 420 | # 421 | # UTF-8 mode also changes the encoding of stdin/stdout/stdout which must 422 | # be taken into account when using stdandard I/O. 423 | p_args = ['-X', 'utf8', '-m', 'kernprof', '-lvb', '-o', self.DATAPATH] 424 | 425 | if os.name == 'nt': 426 | # On Windows, one has to replace backslashes by slashes to avoid 427 | # confusion with escape characters (otherwise, for example, '\t' 428 | # will be interpreted as a tabulation): 429 | p_args.append(osp.normpath(filename).replace(os.sep, '/')) 430 | else: 431 | p_args.append(filename) 432 | if args: 433 | p_args.extend(programs.shell_split(args)) 434 | 435 | executable = self.get_conf('executable', section='main_interpreter') 436 | if executable.endswith('spyder.exe'): 437 | # py2exe distribution 438 | executable = 'python.exe' 439 | 440 | logger.debug(f'Starting process with {executable=} and {p_args=}') 441 | self.process.start(executable, p_args) 442 | 443 | running = self.process.waitForStarted() 444 | self.set_running_state(running) 445 | self.timer.timeout.connect(self.update_timer) 446 | self.timer.start(1000) 447 | 448 | if not running: 449 | QMessageBox.critical(self, _("Error"), 450 | _("Process failed to start")) 451 | 452 | def set_running_state(self, state=True): 453 | self.start_action.setEnabled(not state) 454 | self.stop_action.setEnabled(state) 455 | 456 | def read_output(self, error=False): 457 | if error: 458 | self.process.setReadChannel(QProcess.StandardError) 459 | else: 460 | self.process.setReadChannel(QProcess.StandardOutput) 461 | qba = QByteArray() 462 | while self.process.bytesAvailable(): 463 | if error: 464 | qba += self.process.readAllStandardError() 465 | else: 466 | qba += self.process.readAllStandardOutput() 467 | # encoding: Python process is started with UTF-8 mode 468 | text = str(qba.data(), encoding="utf-8") 469 | if error: 470 | self.error_output += text 471 | else: 472 | self.output += text 473 | 474 | def finished(self): 475 | self.timer.stop() 476 | self.set_running_state(False) 477 | self.output = self.error_output + self.output 478 | if not self.output == 'aborted': 479 | elapsed = str(datetime.now() - self.started_time).split(".")[0] 480 | self.show_data(justanalyzed=True) 481 | self.datelabel.setText(_(f'Profiling finished after {elapsed}')) 482 | self.show_errorlog() # If errors occurred, show them. 483 | self.sig_finished.emit() 484 | 485 | def kill_if_running(self): 486 | self.datelabel.setText(_('Profiling aborted.')) 487 | if self.process is not None: 488 | if self.process.state() == QProcess.Running: 489 | self.process.kill() 490 | self.output = 'aborted' 491 | self.process.waitForFinished() 492 | 493 | @on_conf_change(section='pythonpath_manager', option='spyder_pythonpath') 494 | def _update_pythonpath(self, value): 495 | self.pythonpath = value 496 | 497 | def clear_data(self): 498 | self.datatree.clear() 499 | self.clear_action.setEnabled(False) 500 | self.log_action.setEnabled(False) 501 | self.save_action.setEnabled(False) 502 | self.output = '' 503 | 504 | def show_data(self, justanalyzed=False): 505 | if not justanalyzed: 506 | self.clear_data() 507 | output_exists = self.output is not None and len(self.output) > 0 508 | self.clear_action.setEnabled(output_exists) 509 | self.log_action.setEnabled(output_exists) 510 | self.save_action.setEnabled(output_exists) 511 | 512 | self.kill_if_running() 513 | filename = str(self.filecombo.currentText()) 514 | if not filename: 515 | return 516 | 517 | self.datatree.load_data(self.DATAPATH) 518 | QApplication.processEvents() 519 | self.datatree.show_tree() 520 | 521 | text_style = "%s " 522 | date_text = text_style % time.strftime("%d %b %Y %H:%M", 523 | time.localtime()) 524 | self.datelabel.setText(date_text) 525 | 526 | def save_data(self): 527 | """Save data.""" 528 | if not self.output: 529 | self.datelabel.setText(_("Nothing to save")) 530 | return 531 | 532 | title = _("Save line profiler result") 533 | curr_filename = self.filecombo.currentText() 534 | filename, _selfilter = getsavefilename( 535 | self, 536 | title, 537 | f'{curr_filename}_lineprof.txt', 538 | _("LineProfiler result") + " (*.txt)", 539 | ) 540 | 541 | if filename: 542 | with open(filename, 'w') as f: 543 | # for some weird reason, everything is double spaced on Win 544 | results = self.output 545 | results = results.replace('\r', '') 546 | f.write(results) 547 | 548 | self.datelabel.setText(_(f"Saved results to {filename}")) 549 | 550 | def update_actions(self): 551 | pass 552 | 553 | 554 | class LineProfilerDataTree(QTreeWidget): 555 | """ 556 | Convenience tree widget (with built-in model) 557 | to store and view line profiler data. 558 | """ 559 | sig_edit_goto_requested = Signal(str, int, str) 560 | 561 | def __init__(self, parent=None): 562 | QTreeWidget.__init__(self, parent) 563 | self.header_list = [ 564 | _('Line #'), _('Hits'), _('Time (ms)'), _('Per hit (ms)'), 565 | _('% Time'), _('Line contents')] 566 | self.stats = None # To be filled by self.load_data() 567 | self.max_time = 0 # To be filled by self.load_data() 568 | self.header().setDefaultAlignment(Qt.AlignCenter) 569 | self.setColumnCount(len(self.header_list)) 570 | self.setHeaderLabels(self.header_list) 571 | self.clear() 572 | self.itemClicked.connect(self.on_item_clicked) 573 | 574 | def show_tree(self): 575 | """Populate the tree with line profiler data and display it.""" 576 | self.clear() # Clear before re-populating 577 | self.setItemsExpandable(True) 578 | self.setSortingEnabled(False) 579 | self.populate_tree() 580 | self.expandAll() 581 | for col in range(self.columnCount()-1): 582 | self.resizeColumnToContents(col) 583 | if self.topLevelItemCount() > 1: 584 | self.collapseAll() 585 | self.setSortingEnabled(True) 586 | self.sortItems(COL_POS, Qt.AscendingOrder) 587 | 588 | def load_data(self, profdatafile): 589 | """Load line profiler data saved by kernprof module""" 590 | # lstats has the following layout : 591 | # lstats.timings = 592 | # {(filename1, line_no1, function_name1): 593 | # [(line_no1, hits1, total_time1), 594 | # (line_no2, hits2, total_time2)], 595 | # (filename2, line_no2, function_name2): 596 | # [(line_no1, hits1, total_time1), 597 | # (line_no2, hits2, total_time2), 598 | # (line_no3, hits3, total_time3)]} 599 | # lstats.unit = time_factor 600 | with open(profdatafile, 'rb') as fid: 601 | lstats = pickle.load(fid) 602 | 603 | # First pass to group by filename 604 | self.stats = dict() 605 | linecache.checkcache() 606 | for func_info, stats in lstats.timings.items(): 607 | # func_info is a tuple containing (filename, line, function anme) 608 | filename, start_line_no = func_info[:2] 609 | 610 | # Read code 611 | start_line_no -= 1 # include the @profile decorator 612 | all_lines = linecache.getlines(filename) 613 | block_lines = inspect.getblock(all_lines[start_line_no:]) 614 | 615 | # Loop on each line of code 616 | func_stats = [] 617 | func_total_time = 0.0 618 | next_stat_line = 0 619 | for line_no, code_line in enumerate(block_lines): 620 | line_no += start_line_no + 1 # Lines start at 1 621 | code_line = code_line.rstrip('\n') 622 | if (next_stat_line >= len(stats) 623 | or line_no != stats[next_stat_line][0]): 624 | # Line didn't run 625 | hits, line_total_time, time_per_hit = None, None, None 626 | else: 627 | # Compute line times 628 | hits, line_total_time = stats[next_stat_line][1:] 629 | line_total_time *= lstats.unit 630 | time_per_hit = line_total_time / hits 631 | func_total_time += line_total_time 632 | next_stat_line += 1 633 | func_stats.append( 634 | [line_no, code_line, line_total_time, time_per_hit, 635 | hits]) 636 | 637 | # Compute percent time 638 | for line in func_stats: 639 | line_total_time = line[2] 640 | if line_total_time is None: 641 | line.append(None) 642 | else: 643 | line.append(line_total_time / func_total_time) 644 | 645 | # Fill dict 646 | self.stats[func_info] = [func_stats, func_total_time] 647 | 648 | def fill_item(self, item, filename, line_no, code, time, percent, perhit, 649 | hits): 650 | item.setData(COL_POS, Qt.UserRole, (osp.normpath(filename), line_no)) 651 | 652 | item.setData(COL_NO, Qt.DisplayRole, line_no) 653 | 654 | item.setData(COL_LINE, Qt.DisplayRole, code) 655 | 656 | if percent is None: 657 | percent = '' 658 | else: 659 | percent = '%.1f' % (100 * percent) 660 | item.setData(COL_PERCENT, Qt.DisplayRole, percent) 661 | item.setTextAlignment(COL_PERCENT, Qt.AlignCenter) 662 | 663 | if time is None: 664 | time = '' 665 | else: 666 | time = '%.3f' % (time * 1e3) 667 | item.setData(COL_TIME, Qt.DisplayRole, time) 668 | item.setTextAlignment(COL_TIME, Qt.AlignCenter) 669 | 670 | if perhit is None: 671 | perhit = '' 672 | else: 673 | perhit = '%.3f' % (perhit * 1e3) 674 | item.setData(COL_PERHIT, Qt.DisplayRole, perhit) 675 | item.setTextAlignment(COL_PERHIT, Qt.AlignCenter) 676 | 677 | if hits is None: 678 | hits = '' 679 | else: 680 | hits = '%d' % hits 681 | item.setData(COL_HITS, Qt.DisplayRole, hits) 682 | item.setTextAlignment(COL_HITS, Qt.AlignCenter) 683 | 684 | def populate_tree(self): 685 | """Create each item (and associated data) in the tree""" 686 | if not self.stats: 687 | warn_item = TreeWidgetItem(self) 688 | warn_item.setData( 689 | 0, Qt.DisplayRole, 690 | _('No timings to display. ' 691 | 'Did you forget to add @profile decorators ?') 692 | .format(url=WEBSITE_URL)) 693 | warn_item.setFirstColumnSpanned(True) 694 | warn_item.setTextAlignment(0, Qt.AlignCenter) 695 | font = warn_item.font(0) 696 | font.setStyle(QFont.StyleItalic) 697 | warn_item.setFont(0, font) 698 | return 699 | 700 | try: 701 | monospace_font = self.window().editor.get_plugin_font() 702 | except AttributeError: # If run standalone for testing 703 | monospace_font = QFont("Courier New") 704 | monospace_font.setPointSize(10) 705 | 706 | for func_index, stat_item in enumerate(self.stats.items()): 707 | # Function name and position 708 | func_info, func_data = stat_item 709 | filename, start_line_no, func_name = func_info 710 | func_stats, func_total_time = func_data 711 | func_item = TreeWidgetItem(self) 712 | func_item.setData( 713 | 0, Qt.DisplayRole, 714 | _('{func_name} ({time_ms:.3f}ms) in file "{filename}", ' 715 | 'line {line_no}').format( 716 | filename=filename, 717 | line_no=start_line_no, 718 | func_name=func_name, 719 | time_ms=func_total_time * 1e3)) 720 | func_item.setFirstColumnSpanned(True) 721 | func_item.setData(COL_POS, Qt.UserRole, 722 | (osp.normpath(filename), start_line_no)) 723 | 724 | # For sorting by time 725 | func_item.setData(COL_TIME, Qt.DisplayRole, func_total_time * 1e3) 726 | func_item.setData(COL_PERCENT, Qt.DisplayRole, 727 | func_total_time * 1e3) 728 | 729 | if self.parent().use_colors: 730 | color_index = func_index % len(COLOR_CYCLE) 731 | else: 732 | color_index = 0 733 | func_color = COLOR_CYCLE[color_index] 734 | 735 | # Lines of code 736 | for line_info in func_stats: 737 | line_item = TreeWidgetItem(func_item) 738 | (line_no, code_line, line_total_time, time_per_hit, 739 | hits, percent) = line_info 740 | self.fill_item( 741 | line_item, filename, line_no, code_line, 742 | line_total_time, percent, time_per_hit, hits) 743 | 744 | # Color background 745 | if line_total_time is not None: 746 | alpha = percent 747 | color = QColor(func_color) 748 | color.setAlphaF(alpha) # Returns None 749 | color = QBrush(color) 750 | for col in range(self.columnCount()): 751 | line_item.setBackground(col, color) 752 | else: 753 | 754 | for col in range(self.columnCount()): 755 | line_item.setForeground(col, CODE_NOT_RUN_COLOR) 756 | 757 | # Monospace font for code 758 | line_item.setFont(COL_LINE, monospace_font) 759 | 760 | def on_item_clicked(self, item): 761 | data = item.data(COL_POS, Qt.UserRole) 762 | if data is None or len(data) < 2: 763 | return 764 | filename, line_no = data 765 | self.sig_edit_goto_requested.emit(filename, line_no, '') 766 | 767 | 768 | # ============================================================================= 769 | # Tests 770 | # ============================================================================= 771 | 772 | profile = lambda x: x # dummy profile wrapper to make script load externally 773 | 774 | 775 | @profile 776 | def primes(n): 777 | """ 778 | Simple test function 779 | Taken from http://www.huyng.com/posts/python-performance-analysis/ 780 | """ 781 | if n==2: 782 | return [2] 783 | elif n<2: 784 | return [] 785 | s=list(range(3,n+1,2)) 786 | mroot = n ** 0.5 787 | half=(n+1)//2-1 788 | i=0 789 | m=3 790 | while m <= mroot: 791 | if s[i]: 792 | j=(m*m-3)//2 793 | s[j]=0 794 | while j