├── 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 | 
6 | [](https://www.anaconda.com/download/)
7 | [](https://www.anaconda.com/download/)
8 | [](https://pypi.python.org/pypi/spyder-line-profiler)
9 | [](https://gitter.im/spyder-ide/public)
10 | [](#backers)
11 | [](#sponsors)
12 |
13 | ## Build status
14 |
15 | [](https://github.com/spyder-ide/spyder-line-profiler/actions?query=workflow%3A%22Windows+tests%22)
16 | [](https://github.com/spyder-ide/spyder-line-profiler/actions?query=workflow%3A%22Linux+tests%22)
17 | [](https://github.com/spyder-ide/spyder-line-profiler/actions?query=workflow%3A%22Macos+tests%22)
18 | [](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 | 
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 | [](https://www.quansight.com/)[](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 | [](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