pax_global_header00006660000000000000000000000064147635613460014531gustar00rootroot0000000000000052 comment=172e55fe77448710446a26b73e212a4e4b6da61a spyder-line-profiler-0.4.1/000077500000000000000000000000001476356134600156065ustar00rootroot00000000000000spyder-line-profiler-0.4.1/.github/000077500000000000000000000000001476356134600171465ustar00rootroot00000000000000spyder-line-profiler-0.4.1/.github/FUNDING.yml000066400000000000000000000000301476356134600207540ustar00rootroot00000000000000open_collective: spyder spyder-line-profiler-0.4.1/.github/PULL_REQUEST_TEMPLATE.md000066400000000000000000000010361476356134600227470ustar00rootroot00000000000000## Description of Changes ### Affirmation By submitting this Pull Request or typing my (user)name below, I affirm the [Developer Certificate of Origin](https://developercertificate.org) with respect to all commits and content included in this PR, and understand I am releasing the same under Spyder's MIT (Expat) license. I certify the above statement is true and correct: spyder-line-profiler-0.4.1/.github/scripts/000077500000000000000000000000001476356134600206355ustar00rootroot00000000000000spyder-line-profiler-0.4.1/.github/scripts/generate-without-spyder.py000066400000000000000000000012201476356134600260010ustar00rootroot00000000000000#!/usr/bin/env python # # Copyright (c) Spyder Project Contributors # Licensed under the terms of the MIT License # (see LICENSE.txt for details) """Script to generate requirements/without-spyder.txt""" import re from pathlib import Path rootdir = Path(__file__).parents[2] input_filename = rootdir / 'requirements' / 'conda.txt' output_filename = rootdir / 'requirements' / 'without-spyder.txt' with open(input_filename) as infile: with open(output_filename, 'w') as outfile: for line in infile: package_name = re.match('[-a-z0-9_]*', line).group(0) if package_name != 'spyder': outfile.write(line) spyder-line-profiler-0.4.1/.github/workflows/000077500000000000000000000000001476356134600212035ustar00rootroot00000000000000spyder-line-profiler-0.4.1/.github/workflows/run-tests.yml000066400000000000000000000074441476356134600237030ustar00rootroot00000000000000name: Run tests on: push: branches: - master pull_request: branches: - master jobs: main: strategy: fail-fast: false matrix: OS: ['ubuntu', 'macos', 'windows'] PYTHON_VERSION: ['3.9', '3.10', '3.11'] SPYDER_SOURCE: ['conda', 'git'] name: ${{ matrix.OS }} py${{ matrix.PYTHON_VERSION }} spyder-from-${{ matrix.SPYDER_SOURCE }} runs-on: ${{ matrix.OS }}-latest env: CI: True PYTHON_VERSION: ${{ matrix.PYTHON_VERSION }} steps: - name: Checkout branch uses: actions/checkout@v4 with: path: 'spyder-line-profiler' - name: Install System Packages if: matrix.OS == 'ubuntu' run: | sudo apt-get update --fix-missing sudo apt-get install -qq pyqt5-dev-tools libxcb-xinerama0 xterm --fix-missing - name: Install Conda uses: conda-incubator/setup-miniconda@v3 with: miniforge-version: latest auto-update-conda: true python-version: ${{ matrix.PYTHON_VERSION }} - name: Checkout Spyder from git if: matrix.SPYDER_SOURCE == 'git' uses: actions/checkout@v4 with: repository: 'spyder-ide/spyder' path: 'spyder' - name: Install Spyder's dependencies (main) if: matrix.SPYDER_SOURCE == 'git' shell: bash -l {0} run: conda env update --file spyder/requirements/main.yml - name: Install Spyder's dependencies (Linux) if: matrix.SPYDER_SOURCE == 'git' && matrix.OS == 'ubuntu' shell: bash -l {0} run: conda env update --file spyder/requirements/linux.yml - name: Install Spyder's dependencies (Mac / Windows) if: matrix.SPYDER_SOURCE == 'git' && matrix.OS != 'ubuntu' shell: bash -l {0} run: conda env update --file spyder/requirements/${{ matrix.OS }}.yml - name: Install Spyder from source if: matrix.SPYDER_SOURCE == 'git' shell: bash -l {0} run: pip install --no-deps spyder - name: Install plugin dependencies (without Spyder) if: matrix.SPYDER_SOURCE == 'git' shell: bash -l {0} run: | python spyder-line-profiler/.github/scripts/generate-without-spyder.py conda install --file spyder-line-profiler/requirements/without-spyder.txt -y - name: Install plugin dependencies if: matrix.SPYDER_SOURCE == 'conda' shell: bash -l {0} run: conda install --file spyder-line-profiler/requirements/conda.txt -y - name: Install test dependencies shell: bash -l {0} run: | conda install nomkl -y -q conda install --file spyder-line-profiler/requirements/tests.txt -y - name: Install plugin shell: bash -l {0} run: pip install --no-deps spyder-line-profiler - name: Show environment information shell: bash -l {0} run: | conda info conda list - name: Run tests (Linux) if: matrix.OS == 'ubuntu' uses: nick-fields/retry@v3 with: timeout_minutes: 10 max_attempts: 3 shell: bash command: | . ~/.profile xvfb-run --auto-servernum pytest spyder-line-profiler/spyder_line_profiler -vv - name: Run tests (MacOS) if: matrix.OS == 'macos' uses: nick-fields/retry@v3 with: timeout_minutes: 10 max_attempts: 3 shell: bash command: | . ~/.profile pytest spyder-line-profiler/spyder_line_profiler -x -vv - name: Run tests (Windows) if: matrix.OS == 'windows' uses: nick-fields/retry@v3 with: timeout_minutes: 10 max_attempts: 3 command: pytest spyder-line-profiler/spyder_line_profiler -x -vv spyder-line-profiler-0.4.1/.gitignore000066400000000000000000000005241476356134600175770ustar00rootroot00000000000000*.py[cod] # C extensions *.so # Packages *.egg *.egg-info dist build eggs parts bin var sdist develop-eggs .installed.cfg lib lib64 __pycache__ # Installer logs pip-log.txt # Unit test / coverage reports .coverage .tox nosetests.xml # Translations *.mo # Mr Developer .mr.developer.cfg .project .pydevproject # Linux files .directoryspyder-line-profiler-0.4.1/AUTHORS.md000066400000000000000000000005461476356134600172620ustar00rootroot00000000000000# Authors ## Original author * Joseph Martinot-Lagarde ([@Nodd](http://github.com/Nodd)) ## Current maintainers * Jitse Niesen ([@jitseniesen](https://github.com/jitseniesen)) * The [Spyder Development Team](https://github.com/spyder-ide) * The [spyder-line-profiler Contributors](https://github.com/spyder-ide/spyder-line-profiler/graphs/contributors) spyder-line-profiler-0.4.1/CHANGELOG.md000066400000000000000000000220401476356134600174150ustar00rootroot00000000000000# History of changes ## Version 0.4.1 (2025/03/10) This release contains some bug fixes. Thanks to [@rear1019](https://github.com/rear1019) who contributed all the changes except for the last PR. ### Bug fixes * Fix go-to-line when clicking entries ([PR 94](https://github.com/spyder-ide/spyder-line-profiler/pull/94)) * 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)) * Consistently use UTF-8 encoding even if the system default is different ([PR 99](https://github.com/spyder-ide/spyder-line-profiler/pull/99)) * Fix runtime dependency check ([PR 95](https://github.com/spyder-ide/spyder-line-profiler/pull/95)) ### Maintenance * 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)) * Fix GitHub automatic testing ([PR 97](https://github.com/spyder-ide/spyder-line-profiler/pull/97)) ## Version 0.4.0 (2024/09/03) This release updates the plugin to be used with Spyder 6 and fixes a bug. ### Bug fix * 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)) ### Maintenance * 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)) * Thanks to [Reinert Huseby Karlsen](https://github.com/rhkarls) and [Simon Kern](https://github.com/skjerns) for help with this! ## Version 0.3.2 (2023/06/24) This version contains some bug fixes and is compatible with Spyder 5.4. ### Bug fixes * 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)) * 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)) * 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)) * 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)) ### Maintenance * 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)) * Remove last bits of Python 2 support ([PR 68](https://github.com/spyder-ide/spyder-line-profiler/pull/68)) * 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)) * Update GitHub test action ([PR 76](https://github.com/spyder-ide/spyder-line-profiler/pull/76)) ## Version 0.3.1 (2022/08/07) 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)). ## Version 0.3.0 (2022/06/03) This version is compatible with Spyder 5.2 and 5.3. ### Issues Closed * [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)) * [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)) * [Issue 48](https://github.com/spyder-ide/spyder-line-profiler/issues/48) - Correctly register shortcuts * [Issue 27](https://github.com/spyder-ide/spyder-line-profiler/issues/27) - saving profiling results * [Issue 25](https://github.com/spyder-ide/spyder-line-profiler/issues/25) - Text box for file to be profiled accept directories In this release 5 issues were closed. ### Pull Requests Merged * [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) * [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) * [PR 60](https://github.com/spyder-ide/spyder-line-profiler/pull/60) - PR: Remove outdated `conda.recipe` directory, by [@dalthviz](https://github.com/dalthviz) * [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)) In this release 4 pull requests were closed. ## Version 0.2.1 (2020/04/28) This release fixes some compatibility issues with Spyder 4.1 and some other bugs. ### Issues Closed * [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)) * [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)) * [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)) * [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)) In this release 4 issues were closed. ### Pull Requests Merged * [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)) * [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)) * [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)) * [PR 43](https://github.com/spyder-ide/spyder-line-profiler/pull/43) - PR: Fix invalid escape sequence in regex string * [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)) In this release 5 pull requests were closed. ## Version 0.2.0 (2019/12/18) This release updates the plugin to be used with Spyder 4 and fixes some bugs. ### Issues Closed * [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)) * [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)) In this release 2 issues were closed. ### Pull Requests Merged * [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)) * [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)) * [PR 31](https://github.com/spyder-ide/spyder-line-profiler/pull/31) - PR: Fix continuous integration services * [PR 30](https://github.com/spyder-ide/spyder-line-profiler/pull/30) - PR: "Profile by line" Button Behavior * [PR 24](https://github.com/spyder-ide/spyder-line-profiler/pull/24) - Update readme: Plugin can now be installed using conda or pip * [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)) In this release 6 pull requests were closed. ## Version 0.1.1 (2017/03/26) This version improves the packaging. The code itself was not changed. ### Pull Requests Merged * [PR 22](https://github.com/spyder-ide/spyder-line-profiler/pull/22) - Install tests alongside package In this release 1 pull request was closed. ## Version 0.1.0 (2017/03/22) Initial release. spyder-line-profiler-0.4.1/CONTRIBUTING.md000066400000000000000000000044711476356134600200450ustar00rootroot00000000000000# Contributing to the Spyder line profiler plugin :+1::tada: First off, thanks for taking the time to contribute to the Spyder Line Profiler plugin! :tada::+1: ## General guidelines for contributing The Spyder line profiler plugin is developed as part of the wider Spyder project. In general, the guidelines for contributing to Spyder also apply here. Specifically, all contributors are expected to abide by [Spyder's Code of Conduct](https://github.com/spyder-ide/spyder/blob/master/CODE_OF_CONDUCT.md). There are many ways to contribute and all are valued and welcome. You can help other users, write documentation, spread the word, submit helpful issues on the [issue tracker](https://github.com/spyder-ide/spyder-line-profiler/issues) with problems you encounter or ways to improve the plugin, test the development version, or submit a pull request on GitHub. The rest of this document explains how to set up a development environment. ## Setting up a development environment This section explains how to set up a conda environment to run and work on the development version of the Spyder line profiler plugin. ### Creating a conda environment This creates a new conda environment with the name `spyderlp-dev`. ```bash $ conda create -n spyderlp-dev -c conda-forge python=3.9 $ conda activate spyderlp-dev ``` ### Cloning the repository This creates a new directory `spyder-line-profiler` with the source code of the Spyder line profiler plugin. ```bash $ git clone https://github.com/spyder-ide/spyder-line-profiler.git $ cd spyder-line-profiler ``` ### Installing dependencies This installs Spyder, line_profiler and all other plugin dependencies into the conda environment previously created, using the conda-forge channel. ```bash $ conda install -c conda-forge --file requirements/conda.txt ``` ### Installing the plugin This installs the Spyder line profiler plugin so that Spyder will use it. ```bash $ pip install --no-deps -e . ``` ### Running Spyder You are done! You can run Spyder as normal and it should load the line profiler plugin. ```bash $ spyder ``` ### Running Tests This command installs the test dependencies into your conda environment, using the conda-forge channel. ```bash $ conda install -c conda-forge --file requirements/tests.txt ``` You can now run the tests with a simple ```bash $ pytest ``` spyder-line-profiler-0.4.1/LICENSE.txt000066400000000000000000000021061476356134600174300ustar00rootroot00000000000000The MIT License (MIT) Copyright (c) 2013 Spyder Project Contributors Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. spyder-line-profiler-0.4.1/MANIFEST.in000066400000000000000000000001241476356134600173410ustar00rootroot00000000000000include CHANGELOG.md LICENSE.txt README.md recursive-include spyder_line_profiler * spyder-line-profiler-0.4.1/README.md000077500000000000000000000064131476356134600170740ustar00rootroot00000000000000# Spyder line profiler plugin ## Project details ![license](https://img.shields.io/pypi/l/spyder-line-profiler.svg) [![conda version](https://img.shields.io/conda/v/conda-forge/spyder-line-profiler.svg)](https://www.anaconda.com/download/) [![download count](https://img.shields.io/conda/d/conda-forge/spyder-line-profiler.svg)](https://www.anaconda.com/download/) [![pypi version](https://img.shields.io/pypi/v/spyder-line-profiler.svg)](https://pypi.python.org/pypi/spyder-line-profiler) [![Join the chat at https://gitter.im/spyder-ide/public](https://badges.gitter.im/spyder-ide/spyder.svg)](https://gitter.im/spyder-ide/public) [![OpenCollective Backers](https://opencollective.com/spyder/backers/badge.svg?color=blue)](#backers) [![OpenCollective Sponsors](https://opencollective.com/spyder/sponsors/badge.svg?color=blue)](#sponsors) ## Build status [![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) [![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) [![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) [![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) ## Description This is a plugin to run the Python [line_profiler](https://pypi.python.org/pypi/line_profiler) from within the Python IDE [Spyder](https://github.com/spyder-ide/spyder). The code is an adaptation of the profiler plugin integrated in Spyder. ## Installation To install this plugin, you can use either ``pip`` or ``conda`` package managers, as follows: Using conda (the recommended way!): ``` conda install spyder-line-profiler -c conda-forge ``` Using pip: ``` pip install spyder-line-profiler ``` ## Usage Add a `@profile` decorator to the functions that you wish to profile then Shift+F10 (line profiler default) to run the profiler on the current script, or go to `Run > Run line profiler`. The results will be shown in a dockwidget, grouped by function. Lines with a stronger color take more time to run. ## Screenshot ![Screenshot of spyder-line-profiler plugin showing profiler results](./img_src/screenshot_profiler.png) ## Contributing Everyone is welcome to contribute! ## Sponsors Spyder and its subprojects are funded thanks to the generous support of [![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/) and the donations we have received from our users around the world through [Open Collective](https://opencollective.com/spyder/): [![Sponsors](https://opencollective.com/spyder/sponsors.svg)](https://opencollective.com/spyder#support) spyder-line-profiler-0.4.1/RELEASE.md000066400000000000000000000044331476356134600172140ustar00rootroot00000000000000# Release Follow these steps to release a new version of spyder-line-profiler. In the commands below, replace `X.Y.Z` with the release version when needed. **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. ## PyPI and GitHub 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 * Close [milestone on Github](https://github.com/spyder-ide/spyder-line-profiler/milestones) * Update local repo ```bash git restore . && git switch master && git pull upstream master ``` * Clean local repo ```bash git clean -xfdi ``` * Install/upgrade Loghub ```bash pip install --upgrade loghub ``` * Update `CHANGELOG.md` with Loghub: `loghub spyder-ide/spyder-line-profiler --milestone vX.Y.Z` * git add and git commit with "Update Changelog" * Update `__version__` in `__init__.py` (set release version, remove `dev0`) * Create release commit ```bash git commit -am "Release X.Y.Z" ``` * Update the packaging stack ```bash python -m pip install --upgrade pip pip install --upgrade --upgrade-strategy eager build setuptools twine wheel ``` * Build source distribution and wheel ```bash python -bb -X dev -W error -m build ``` * Check distribution archives ```bash twine check --strict dist/* ``` * Upload distribution packages to PyPI ```bash twine upload dist/* ``` * Create release tag ```bash git tag -a vX.Y.Z -m "Release X.Y.Z" ``` * Update `__version__` in `__init__.py` (add `.dev0` and increment minor) * Create `Back to work` commit ```bash git commit -am "Back to work" ``` * Push new release commits and tags to `master` ```bash git push upstream master --follow-tags ``` * Create a [GitHub release](https://github.com/spyder-ide/spyder-line-profiler/releases) from the tag ## Conda-Forge To release a new version of `spyder-line-profiler` on Conda-Forge: * 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. Merging this PR will update the respective Conda-Forge package.spyder-line-profiler-0.4.1/codecov.yml000066400000000000000000000004311476356134600177510ustar00rootroot00000000000000# See https://docs.codecov.io/docs/codecovyml-reference coverage: status: project: default: threshold: 5% # allow for 5% decrease in total coverage patch: default: target: 50% # require 50% of diff to be covered comment: layout: "files" spyder-line-profiler-0.4.1/img_src/000077500000000000000000000000001476356134600172315ustar00rootroot00000000000000spyder-line-profiler-0.4.1/img_src/screenshot_profiler.png000066400000000000000000002067631476356134600240340ustar00rootroot00000000000000PNG  IHDRsBITO pHYs+ IDATxgXW೽RЫET`^Mb5$hl%{ iYX޿+.,,EX}~,3wܝrΝ]v  b2-  {Pv  iA  e'   bZOV,#DjhAOT* + rZ:;Ie¼,R!! +_ĸhCh Kʚ ŕɤYi- )Jze; L`hfωv8:߾zNo&6t@/5B$wUK)<)5̅ SÖ ͜6eݡCF'shQqI1wXAQQ0fėɡCFyĺGi4Lvҕ:G 6a|@PՇ7lYj`k>_v%f\pc?p8<VkhFJ46%J%g/Fb,"<1BOcZ0<)Z0OR_ bnn^PPulo蠰ݻ.yS7^n׭{a!}VG9/8l^X(89,Pbzp8\;vwt?ן>y"ߺcwqI X ^Nw?ҨtE J,ء=@^nckҩ#\҉N'LcNFs̱ygSGNZZV#-̮W))Q7o[*ara!}32Vz E7r=L >odBǯT*msg}ճ[WZ}fĞGUj?0߾7J 69 S_U-S&)i6_Xzc'Mo*kWƽ8q<6{Aۼ} Vk}. NܯwO~Yo3~x^e{ uuk--?d4꬗-_xS~V&t.;o޺'sYA"-aU U}ݞdn R+ޚ '@M 4*"gUc5J13dqm򊄕F~Nss^(пߚKG,kտncoMްvŽGNҹc/0Fĝ.:dp9ev8}rӾؘ?5Lolvm̜X u?4aCoNB%lɒ??{1q^&Ol3mD|ɂoen`2 eⴙƴ3t\6"*YO6|MR7[SQ! Sg\pb΢srҩ#ztvN=b/M1;k9T*Uêꍈ)?v6Qц0[::Ϝ$!e]-?jTuԴWOغ~MN~~5N޳{Дo--+K7žFxy¹˗,X_6la0Ocvkiau?y_ֿY9q,~im>#m uٰg1ݺvP8'$ZZZ_jȡGOc<>M덿4j"5dеZ--UzVl lyY@ ڽNji S__?~1~*8:޳?Y_!c'o]FfkNڃBUlsW.+~=19bAU]ZHmg_{Cҗjɦ"flnWxoLy<*6_vNv[rvهZu kI$ 7@ 4[b䋩3k!^$ҲrZsZH,vuv†ߺs_R]m\xlmy=vY?j ޵ˣ8n# ATZ9wJ@PX{"{բT*VMIKW*\]]6o[,TIzW*Z֜<{1ooz#"o/<~ w{bkԀ~E*JC[KWN˔3[.d:{{>\㕜>_o+-+.a*H/edmd2*wpjZzj&>ظnnܺ+c'uԸnd=t?N?v3ܘxZ/-;u_XlkiX{IbrJܠainpTT^ گ60%-kmjk{Jzt3nXQgUV׻Cdr셺/ײPZkM#5# s r@ Z 0JaF Tߺ~ɿe$p ꓨ(]2R+~nNF*,*T%<R)櫖.h4HD-eB'ƚdXS :f?,+zUNR zQћFl6mgpX?e^~` ^J~о?֯_^Ca t X/o@ZV4 H<H$2 M^VfYJH$fUe]X&ҲrRe7ƧCoƨTjګ7TnuC\ =뫩=ævt_6d"D&a_m ;xIyǚU5lCkH-ZP%7yrUfb^k/ګ?F輒7K]BZka5MFnk͆_RRp-/NS7moC vfyX&|&Wvϟ=cޒL8uxZM=~KK [[nf:CJw?鷟S_eeY$AOr^$ھLN{;Ra!} ]K}lVV ++?%#CsV hnRL,r#7jO+)qԹngވXrٳ8F T0j畔%Sg`G` SᗖٽmA$H<~i1JbXYýZxAW]uΎz,vX쯿Ie>K&{auS}yn®ۢd%۽kVY fpKJꮱ:IRTp3"Иz?վ]~@mGN{W#$(kie¢{ VԄ1#,-ͦL~>6zĝn]v6nNl wUg}rͲMȒk[w2dpج)?~ȹ(^8A  U}&WzmA3z:'4:wuv圬_^Da/!an޾3zgNT*eΌi?nݽ6_/"@"۷0k\7B^nƭ;WA˗ ygS7>_/{O޻9ƟFoh4LL"<]ܺ~fdeS'M >mByۨN>>yJr964ZEe@qj\wFjW})lm8ķ] :3bh-CMǮ{PZΖD=i`t %@D̝ ՚/k]m'WoD޽*3 kA17]4GOw_;}ul?|7B"]y+cVt8w|?p^$U-$<:t$6_Z>"oX*=Kwv)ڲœ/tFԓg X,s>uxZ~ ɳv]hX,3իz۳OY/ؿט0CogN;udBsHο7Ϟ5%UaDr8[ׯ1q/~YŘVk4)غCo]DEY͝ʵb$ݻ.3cM{{v%'{~;.\#~[OEK̬ 'i4GOTЋZaϋ;vDyY[vbguמeͿ|hq ?ֽ]:8L&?ϩCęqj\?hΌEfڻūz}o;b ;.JէW/}zvZ˷ۯq|V&h5]vqr9Q5~K =}lzMŘ/:hWտaJ=>DlM?FV/va2ƦzW'5 tz#(pɊDon 2h#mu-H=w6mJc;'ڹD***{ =&y0c[jD"TkQV(h [[[`^Aa:6ZYб4|.ӯWϩ35yTT/B= ^k>UqbMczG{ h?v԰lZ'M[w7Cwsr++8>K)F,{v9l҄ofhAE噋~\_L|?'b 5jpt߰vUL\}z#GЂu◕5{acܹw;n߾ =#Τ|-^4/ZZZŢ߾C*aҴۚ)/k~ h+; D3\A i  G3odC VZ > AA +Z: Ĥ ҚccD$Z:AE3X{g7^=$Z>Y86v(AAA>D,V4 wOջAA4AAe'   bZPv  iA  e'   bZPv  iA  e'   bZPv  iA  e'   bZPv  iA  e'   bZPv  i!t |tębÝ\2gn~-^$V/3b(Bqw㟻Tju-"H:Ww󶿧L'+4x./*-+:s9/<Aؑw~7QƎVe'8N$[[Y-_2FVkGOWidiiqngGN9@>jlr-[Kg/"ء:;nL|9i޷R+ Y艉}f%rճ f57E?Y0/{I|%1aΉ'1 _{U#&LK 5|Ȝ˄BqƆ+{:܌䞗{MQPXvcF1AĽذyr݃;w0ihwX.U`̌-ؑOpY_iafvFx>zP Lszd2g/\>~F *nF< $6Eko;=Hes~>rxAWGxm`pXȅeddNjC Dߒ_ŗN;)9:8; tp8ܸQ85aC"k8lnO,Lހ2+U3&C{95#ШԠ|N~HR7zļˋx]d?a x5^f"صu}~޼eg]âOL7jDk/OIjA)e}/QAQYӧx21%5op7isCcJJnD9ubՁ>}x>/+9xFmZâwm!2R_fIYٯv>,-3P\R"J<ܰ'Ow׬؄N Y.WG+njVw#b#KRB^cbKTuhTs~CN@RrZ*&Ѩ'K%?׾ vA^u>p[' w?svrR)sfL{I}Njt7aN:'G033k|=,-Zm@h4|Gp :op<7a`v;Ͳ>!V56\nAG(lm8DA7r9fYu }"DգFfڤ" jfݩƛ HIM/(*6 *2d`:mқG\1N,kI{=.Z֮K/ߍlZ+-RP8HjPL~>1c2^Us2g˧`]^Nރ]Aƻu'Җٺ~ F{-uOc⵿oZd9yFxzfBYV~ ΤP(eko43c8gϷڻt\{{[X'aa﷍[׭YY)|o%eee叞7N¢K=xtq0K/]UT:E+׮7."q^}KͻxX"{AC,]ˏK}*p? "xWl.D"޼uX5 عyZ#Lemɂ"X$/Y0eTF"t]UJՙ 7S˔V'tkwڥr?_M@O |Vv֝{~7׆N=ud3sb- ˧#&4Ovqv[b—3k޴Ow ȲqoW9 jh@>%ĺGy_XnݺZBܕ `Ӛ WlhҪ=|`_o߷𻏋J@"w_m<~Yoz9WqU EM2 ;}f# +(.(*= ~{\}R ,;̥܂FVFB-=W㘝ڷ!)%st{틑VcgY[ g{ r~7^\Fgg#ߊ4!9;fzv;< @D5US${lѬIh$J :GGЮ DHݍaIVcgjF*U*l8kvc47S(me2K7x%e3^9rN(L>OJcgUοwrZZ~\u\BJkOWwW⒲G:ؗ>M8zV%5.cZF7jjBx~t_[KsD*q)~\uJz&Y`*S5׆+p l..BЌApus o{ }<0L;&}>˽\pŬ6vh(e%o;jRj N,B+ Z8q9wyA>v-DZtZZF6ՀqajPWg_XmqTho;nDD*{$HZCkApcwr9~= w8ႃ:ҜWRvrxʫ,0pkQ3 *{+5s #d9ɥU_^VbhNx@vN>mjԌ"ю;/;jñJi\޴NB&̣ax?rJqQFqoyӽKVn+mHef &޴о.sjN'JxqCOS)m'H5gڄ5ּhe!=;:xd¨1NKJɌMHW2*~mc ܱD*HCKоW`ianoYnh֤]?~,>P#B({ŏn㰬77w>3d@IcaL?{x7gGݴyE]5 [{ɤ #:xIvS<'ee)W(ټL".RȋfMKLm Aiԅ3w~rXVsB\&ijfCF ӗnz9Wӫ[>w?WXܮ/?_rtȨQݧB(n&Fed2Y3%lY\;x98Q Lh Gva/m֕Wen @1!!Xj<~Ԑiem^X;IT*Jvr#BqA,^e4ک7t޵()-۸߰>jz<œ9vh }rJP**ߜc \R,fnJea+G"QiCoKV(*ZQ(R2RDژ:s)%)C#>kϩT/{K"E' &Ip@g0jd'5WʏR8!ۃD"?V/:XY5NB$y%ubVk6d5TKnލy7N:e-:ͩT ;UTvƐ_doíWɆL"r9y#*:f׾Zh)rdy_NE%9ܝ}E^AqB+pkG$"Qv9j)^+ʬ|g&V+_`~LfYp'lw,6( U*.^|ӿ7X"=y ֖_O3(TujЕDoT!> خ@ vwKF{h)N\R:q5:qɘHjZ, T6>&!Ŀ4viT&FKHi/2zy * `iR4@PaYո*D6^nƗF iTV[)j]ˍ{lbj#6ϓ牅|(99xG;nqǮU;ҲDn?Lhp|R*hcR)ʼnJ!%R(}{@>J K:p82ʅ˶m7N۰,@(*JHhHۉ gO^Q`D­{c>pE<Ո5D$t;rr}Ovʹ#&B&y4ѽXJ:'ЉK7{!JSӳRI~%x4bssfؽ&mcR4@tlb>,pWo;zGOqM',㗩`oñx'U:;x~\7rKJmLɵ@Վ>%uT:zHeGOo~Xn~4eОs qçJjalk R[pBmOfۨ]cШr*)[5>O ήU;ܰMڋ csz t;>t`u[7G;7Ǭ ,1|{| یc^$ՙ|\5A+W&f J>|I\UhGT}T R(*~øRd!H\v_\R Z"qqI)F MHᕔԾ̓;A̙Jћ!l,O7g2TǾ}H@H!<ӵpv=kVRS+5v|#Y)kyŖf}y:+KH]lke%"2zUӖ˲4Dٰm,CىZIL]&WP(dRhtHL 'Ө>;Ē"^FeYYfd'f1t:P^RJUYD={-_jR3tL~nʢGL&WX3;zGF?pXݻ]U-bXRHk_"1x9;IDDWs32B.W0t RZMKJU~>q6#;O PĠjST14[._VXAmH$1c2$RY]R^eHaYi܂byv\vdr_BШMsHQ /Vk4 B,- yNR-.)\U!r)*"k,4SvҧgwgGC;t꿯rs ,-ͱTH ThTH$S̫1${H&hkk \}tA "1Aٙy\VyEX"hT*j'\Baei}:ޅh uɄB$H@&,K]@ IK|-K t睨TjJ_c[Yz9gj"ݣZHD^3& ˶nha aryk]0F~!/=\yﱡ1+"G; 3؄dۚee]*I#Hs{3%>"RW,eRk4UՂ&.N,#NJ܌ 2^7eyic22^G&4Cjd4':Bɠ xTV\Rdo_S* IDATv\Q)NXPL8\ճID{XYөT~JlfL:LJRt0c2t{riɡDR-m ކ+-r PA a[[b]7tP[owg;I|5Zm!Z hTjuMBɠg@U(JzgkO7Fd=B|YK=8Amm'``W Eq/ӺxVkSɯ}RH"啔~V]va>^<v4'b4;@ΆݮBA;AA`ILDDż:# i@-l   bZPv  iA  Ŵz"^];˰|АxNV[Q)LJ(3izBp :߅kwVok؄݋?4W'{WGc5o“!WYJ$ҤLTf:]yO̜7n?a 'q4rEjZ&oigæ2<+UVP)d_o+ s |#t;1 MybRABi)|;n>W""Zj4A 8\nY -̘=$l+ʊJJ]챔׌A77c>KwWG.ޣJ۽ۻmUlkK7'[  o@5H : Vck)x9;IDDWwL _Txb~ƽ>K2XdjoP\TV)&`mJ .˪W۳"k@$ =3x[EbW4jE"ZbTѨT*2̠xImhͿFYX0 b̈ [57hJm6[ܤRk|BdvtwCsK=iCFcYemstؠcNўeƒUjM! "BJAc4~ޢچwTT1ha(J9{ƜqQ>`04H\8"ZqTTRQc67;}#ٻK$Yֶ3y4*5:0.*~@.#  }@Lf3jc'6v y.:_::*~  }Qi~^]vʚaz=ةsUQAA[E'G2Gd6b;/<ؿL N9uݹ1 %bE> ;Wh  ɹB,&FjЁqym7jAAۗs͊% ܣBqWkJ*ȻuitZX2lo~)  ŹŒ}/}}A{  E' 8C*  ғ \},;AAĹAb?{zؿ-=^~cgv=OGĭ-d:^8uaZp P;߯;~&ZȜ{LΘB9զs +?n?+.@RyG*e3_SiZWM+nQmӎw&q<_[=]@'/:`MPRf ͧƦ?_IϽksc3aQٟK.UmcDbFgu7sBdag\2\SlMJ {ȤQQO|6ךW-eSTg ξw/;~DD*5Il8GF/vxM)_wf?Fj85N?&V'_ԧU e l5Q)1ߛ/ْ%m?v86BX 7뗾M}>/Ţ1?vM[lZWt_eF_>~?vn`Kta?6vȸ|>탩*Jk=^ayx|ytNƅ%S.X^\]TXi{l&|ŠBOHVW+H*X&Ai?*} DD0e;DwetWg.STjŌqU~ܑ^^Bx\oXgarXoqݶ->8GSR[,A6}y'ޜNoV#+}m>B_c޵ ܢ3<[m{39 C|aYQM" {yAOjZ^uDbD ;z#FNΘRP'r_&M_VWzdGq)œs{;6G_4jTCۑ]//h6*5FINy6\6Iěu±YJ53p[;2֕t<w43*e3[/b6p F[v ]T]|$Je/a ^|~hCzM+/WU;Oo Swe2m6zv8v%+\Z\S(mB:۹lIo?8Є`-];d?,5fVjl*yHuSAcYWp ~]qMX1!Kν_d3X(Skvy~sc퐨ѩe̜֞n?RC\KP!2dop#dlaۧA8_{I>.+{,^kN{%,;qs&̛3a^vug{=+m촣\V_J&J$r<;II$$x\woeC9T5U|澡wڝ_Mcx}8;?~- }@ '/N%t<|9yFwjJjӗrū? [k[Da DA>w1CՕlupW\]o6u ȻA&;St*]TTi%O ,b,xYt^(8n2_e/}v _}JO ;56#6$6&pk-sq/e'ګN^<=^*d?c8J3z=:aY+Z0NtҸR85vUץť.gxzuvLVKؚ֨X 8'>{jwyCW󲿖JR7!iq, DZ,}_K/cGC>&ARߍ*:0[p  (-7ccn#(W\_8\0g}ߺr\E S呏\T)2 ]^nNıX,8XY@^pya²NgGI[ n^7\Ulbq| nm9~!?\uy=\`з7P(:ntNlQ:Je1ضaAU-Gޙ4c4XT}WOgUXJ> m5u*+<|CB++Xdįb场N;ށՍa>|7[;/iT?f4W\:ek/ fCR.Sn)ώ>fXp4C][q*/TOd/+eҙ"@Zp^{tr,hjl*9]Ox\7%R.pw() uu^|KҨh_/ BfOҗk YTZUBX)޴F$dJ7-wc1 1tb1_tCt:NBjK 8}7Wmޜ/L~ȹCJ򱱏2:OW+).n6ι?1zQ0*Jèitb24FFbƹsF15>mx^7 ژJ5iG;йs{"=aDnљIlٜ[hU-oj-ӗr_-u;^"itW؛60,:+WnrO4lFk"Qi7l#S$mRE ePĠ?9o+=mָ&9n֩SC a m' +t #ΉGyh%}|cGgP\c+i%O}8Ig6ر&3x,F.(XcU7;{߼'{tXi«4zE_kk~Ko̝0STځg%g~m֬"#%+6:7 IDAT~I.KW?k^>Ac|P0*NmYϣYCnEm>zȤQ l'iGuz K{~BDž ̺{57OmqC=~lR؝gͮA<{1 r"rC?j뱭p`n5a~?li鵟޻"H+&\qC2&qA.4|뛿=tn]KC2G>SO|6#"34+RcS_~jl[Nz lhҧ~77 MXѢxa 78h-ڒ?n\NArUMU7?iOz2Bj#EfN`g  HAWvAq.(:A9f{/>ad7Uԗsq:VrÇѬC8ŕ[/LXH7oՅ_{8yO\ْN:a!vS2Q"xw}8GW<Тh_Ex+#(mV1G4Ŕo>^Ot])='Phr;tZN;ށiQύE_|- s \1'%̢^Y).͏c'* eu%P(=ƺ`G;zFtێ{&ܨuHhJO{v} kn NNzyͳN6#%=ԈᏤ<ڽެQc&L] s?+c^ xƲK?%O.~${7UvПN^muQy?8 Re˻?Vu 6[ҦW_Mz~/踉F"8Ay?/JqwGn޸hs_Ʊg1qZ慍n~g2G>qc/KoNळ2ög{MJ:ul@׽ g[x Q.:)ڠ*d:OOcnnʼ|$(SS&e;l2 Exb]I޸5g}Iv7k|nh*kQg4C(w5G~;lmUKGgëΖ崪eU]:2?o?6괣8:nlq#tuЈ4v5̵5*R_Dn^QEjٮse>Q}^q"UJ&S?[ǃ4[,s>i> __?')ttI:Ǻ.~dGO{~r,.0KCmHö}іGO@'ȝ<,&yE|JqY)O:] Wvc9iQOm3[f:5b6˛+˖m룡5 Uy̵}Sqf}JO kex⯝bj jqf=ׇKMy"Ɯ4d 4 Wl6_\Sl*6w\knFQd3X-mWUkUl#8vHW=<_Ʒ{VZK8)= wK)\`K[a `ܺǛߝ>|iK9  ȼ|Us!m#YVg_TkPk #mq3o ƽ6ukoǽ_?ߟߨ7_#tu{M4l|au$1h،0LԙČyi2g9n#ӎwFel aq5z 8A`F̘U`^`pOz7jNl @-0U+IcpƦWw>~q+/׿';ޔT{n޷ŔUtlǿ޹bFa- c.^NZת5zu(R 8)4z'ϋg2 q+;BrEgMLN4p|ov-~z f[o\ٛ-Ǩ8Qp:nXq:N@T:{_HPZWUtȌs\FNbןN^zmv:ށ 9?=zO^PP焓N.i !-mpcғ6Щt0AX{`o?!0 f8-[x߫^ 7z~_(ӥD!?jo555N1^zF=10=c'xޠ=X=+{E_?*#ywvy9t*ˢb4fῳۮ 7׽ԍNzj֧NnX谊}4C#=+TvhU=m'NFRmś&ޘ^OT%=ֿՇu\TIu?SmXF~m˺ӎ@ ;WQ~eĺsXr0rޙ?-nl WѦW}sr}_VAzLtwbNA3Jtt  rC  )fŒ?  NAq6(:AwC;ipzr][6߷}ڔI^Z Wvl3Ç%֭-Y|^t6["bożgL}A:߮2;.\/GE7~yfZ0=mJ^o3F[ށ) {Z$.YE췉ݰ&;f)ޞ^&+~b9^BPngXhHޗVal/urp2{͉S{Ӧ<2LoH3D۸=u:r2/oQ#&Ozߙ1wAl̀9wȀawRglپ%r<3g;0!tVwHoz)Iҏ}Zu3%e|كfk^^t;:c%&mZk}ǐ4mז ~>߭tז D>l?Y֗ W͝ Vv*.s=]'mJqd톍;C0_o_׬Sk4[f f@Əzz-bG'^WF[ށcqБ*zo$z{a]֕(Q(MV׻_mUUua!O̞{}`_a06tH>?7lJҍnٖ"ȝxl{ r12y٧,g`GoSZVfl35XK*fKK+N^mu^{o+Zm!AA}АnJAl8kKZj-;JVq97b,k,>zbܨ.Ss '8|XTRZ-VTY+9nȳwɜ3ry|}y2gWsA杌JO ׬~DFG5ʪj@{-JE6j5XL6l&KQ[*je@6u{JⰯu6꼽vdbEjf^o]Wzry?qd45w@DWאOi4zΙnOKJO'-`9KoPىek%L]o[~귢gENE'G2 Fh5-q1ORYP[oWF^q?v;:ijiw N6P(5Zpzh;Pq8W9\VH`4%RD*㹹+ϿVֱg [$2Otpقc1NR@3 ^CLs؉쏗>=NӧM9 o?8㕝(oC&P+Iw+BmNׇM橬w`sKF &CN^mu^{VVUwssvt_Wz(܎.^ 1 1WjM/Y,:bHA^;:oqHf)A y|]+Lz'<X,&3mЮ"ƹ9mC^.S'{.L r9!AUDt 4Zp|3{L_?(*.khjzrcL& |S4-}b WWQcGu@gfI qSLc촽`CB\NLt'fͽ]^ox흏&?aZJo_,,ڿ^{;7;/O(-/w?\#.*ob-+/\K>|u}쓓9gm 8~ddxؿ~[OO?1%8jW.}EY٧p qGڲaז Ƀ~b֮-~K-ESQlE?H&{ټ 0nBy"̹Y-u&>艓; W'gϜȃ4uc+wxwzt.ˑau ˿FY_}ixjZc߷z'G'>W<^礧3" /_96uڐW}q$3O'ΓZ<׵"Q\  wE'ueAAE' 8  \Pt  sq=Kr9~>J*\’ {9Ӓ=< ,j ;/&*'y Æ$ls1t`DVTVu\ͅ(Mb آ6i"n&:pTRAnb298)ml({Ný= :]W4V֐ =.*K$449.A'U(Tj9N~:P,u|\q< :h2z9 S5sY,׬#qKԫ O.{S]ob1\ipuKwiq褾d2s9XJ"cUjѓ&rju \QaA~UUNOIP(t:vbkS9@O iTjIE_@ Q9lVbl$0 |DiTPUpǰq#1ɉ a4fE8n2J`2G5Lf۫;F{'NE\q o_*X:v/xypX6*72OTUXFD{{yFpD2QiCp(ؘ)TO\e'Rb4{I,9AP( _oF(TsEv6-9t4qGOU "F2t&qHF4"40ϛB42"68^$-成@9Oe]H']*)KZIE'R|ҀlY&sFu=P, }s  twsqr]atzþ"!ؠ&٬RkYc3Y9>"$@d@߭I,uPŃm4ZbLޣ' #8:S(UY,fTP7)㤭ZkfŲpփ2NγM9ߦPyzҒ6:l6I\R,Q(WnwR{'D'NS(ذAq5 ?[aظ"{D&}8‚T';k4b"C$=yV79'V$2yvn~=:ixz>g0c#CEg=S{,gCh_lTs-9y`0 wxAgS'4Kt.幺$F=TEAq NꄜqVltx}wO(uMb\ UrRcGW=䊜srTnAι6TnA7B0yH[6&YcV\^84l1@ys,VwÉg.WFS5\Nw6HdH" ";dKd9i7:8TSXZYcfGd} ,R&NOͮzyT4c]<7ѽvk4PZU)Ш=^, hU(W!U64յGs,Q Z]'uB}4v2*=-ޫ׮}ZZYSUww\M&3 oOaKz )#l89NEne,Aw-.2-1!\.I,Ϳ\ڍْs>,?,8`0U56;Fh4bpX7ӨBoaStVe JB؎XnM4o b?O:&={..ZC>p|;q7 ONtrȯIچبP PjMޕ5:%$FJ% δ)ZAX,2Z6mHVgKd%UjF𫢨21&")6J7:TiJUޥX ( fOw$>:| JK%2u҆LWEUF6BT oN)՚چɄPkGs^U`GxglSj3tbLo0:YQ]GQG'N+T^]706q# 㥒چf{RBy$911јOΞqN1=9V8$y8ڪd!0 f*UeVy7j  H_5vbXݯ%ɭ# rsy' mPt  sA  E' 8  \2t^_YPZYQaAQv:VGA;E'-eU&a%'*TiIEuIE !<8@$tG ܩ.:ڗYSid6s٬fh_׳J  ȝӨjIlb2MDANGɨ?;B^iieMUmݍs3y76K:$" r'HfV1&Q,tUyr_J"  be}a4Z7AAۗsE'8YL:ey7cA5+ @$p q\^*#,&CWXҿ5DA9WtbXs;}Io{o  H?p+;  (:AAĹAA炢AAs͊Es=q:6ͅ(Mb .$AXS Kj qksH NG{{ tNi(l/"$ $ЏF*˥2 ȭ^QhR9sb!:=ʖlo.N,9/pt]CN7U՚Lf.P[JUX=t_m ؉(~@8J0j.Vl%F (L8WPd6;ڜsR@K%d+7.*dP˫mW{(R!rCNHerJ&f5pX6MUu!t:`0kM@?oOkb}I20N*/v!.9@d 㸴f a16K86lP|QYUUm:XҐB`Xzbv%qӨjI -nm e\@ TTU6XB*kَLf_/FS( D!0rFoIXFpQt2*=-ޫ׮}ZZYSUww\M&3 &$#u3y ]IFY9/ə FLd(ò-,E긐3yɉT*^UPTVEFR#y0 KdAlQtr$3]l2M&s}X /* Q>^ÓfA:;(>ZTp vtzCAQYAQ hk꛴:`w4 ]ξ 7ncDXQLKW A'd0ZAYLP@:xzJF6%bpOC0~0 A\Np mln1B>/Ba 䊰 FAєjMWAnhӓqL' :h{eƒUjM! "BZF 8a< cu]cڐ X"mhٜLDX*oc,2tsE'"{Th jRIy _UmHQ5ݙ + H7FSJND&t溆fӝ3NZSTsݲ+ЦPe=GXOAnsE'%;7t K* ,B   K" \Pt  sA  E' 8 SRƑ w*}@iɉ|DȻ 86}ϡ,pQ=8>\Wj3Dx{ eM  Wpop@DȡoAE'ޞB+kJ+k@_:&nXl0bɠQA.֩+;$?&t^uz5,\)6(6\]8 2rr褾d2s9XJ" ?yZML9k rr U[!D:$X7BQtډyֱ &a2.)UcF=rRg6:? NUu ʾ^!~2y+*˫ ?؆  f9"$2sΫnetxpB.4ޢȐ@MfbB?o˥L:Ez%:qlC'ȽGR'4%5[ NH4`2lV3HPSDUIOihA.e0=|Aq\ϝz84F3Q%*hnQl]FJW4ĕI<8hrG{)L^>NF{uOK+kjn|+J7%15.24p:#rѨFvh)p4 Kc#me6A*50aEUHڦPkt`2p;ϕ|l;:B@ձX[u``0i4ӨB:D8NsP3UI vsLb8R5\d2kgm6 Eubuwf. a$:f2%rojIONtᲕ* Ɉ ˻TrL&hrz ^sۚЛ'>.:=Ĺǂ}& PXtf.;)6,ArB>a}Ѓb"C|A$ZQ,ɻX($D1bfCJaU@XTd2g͏LXEeݞ`W^]706q# 㥒 ayR 0"lRw$910 J*w/BHVh-2 VP0 yAJ((ޡqV9.z JׯtX Rb]ӓ! rC#l 8  \Pt  sA  Źf"%2.B`!6\T;;"N IضH Y7mA>A~O\@X/R(\Z^P B"th_(a#7-[T^v݂7gH8 "ЉΗauL w4gt4:a1cF062dJ[ fD?{78PA\uuVvX[G::êuu[;ABvn~ӐPF|{{s=BRqi3|<\ P$/:6sxmoB|=S2UZR*J)).)rwve}D; sUjJT*/Vqs=uJ!G<-`7qC#B#A dqA Y)Jt HR‚|i2-+|1MkbJ+o<0yQhppJD[ U ^nD"AR?Z}|A 沪Z"0.nȓ|;&BifjՀ <?6v0ݜW0Q`0D"A3vg8ڡW:IdrYK `?/2 1r992pX\V%>s3WX: r=+,+qzkMAPm*J퉎 am R(QXPZa|ZDžXYd@D"!6Tj;jx4DR %zF 4@ $"L-Z!~$"U$,h#h`ņҭ-1 /x[V5񷶲F=&h2 lǤ!v,~mF KQ)"x=чT&GX`~QYc3W%K lMȦR#hdsP"+ˆ|B} J*XlXڱ0 򽗞"heGHN8Q( ^WRhT\,;j@ݺe ՙwi'Ҩa3[[]OJ [[C$*:"J!)lZ-3 [!>uOi _Lx/ WP][I M|,hoX+R5rEIA͜+;\=F$}o<T 9øuDc@_%$`UJu=)ua3Wv*6#\y' 4Ш8"lFJ)itk!QafT&pu4Q fXt?wj H` 6s`rbq8\gN~v9M2L&GG@ ^J%0 bU$6QgT"v,K 3Z$9;5s#YtkDG0*L">xCaJlZnm!"[EbDR*D"acb+bB0G iՀR.WWk_X,֎(.V*ƞE B! zf4kiё$wӟhO *D nN4*E,vacUЄTJ%2˫T*5 uj.\U5O R(/FĒ&N *UeM=kIt+@YeG*ZRq-fTFӚzJFT^rdޘ;ibsfNh(¡'5׾u M:"xN{B.!qx6XYxJEQ4xC<~* J*j5gЭFFp8"`<9a|̀&Ngij5@D>juU H8X,VȾը7B&P=\N"i!K!rRRk,gy9{ʚzcn4G3(250L}{)ZbxhKfSdCP.Wn$RBT Yc合DR(zu!z7>;7/;Sn6hT;frcp%q8Ϳ<>}N:2ynAInA5*tkWG/*fw2HsAе~,=mETx BA[H\^= p8W'{4PR54xƥ˕0xr#m[šF]SX,"dXcجԼ/)vwuosxUU ,*j4De#c1̨%Q3^dDWcz+6/>nۉ)[ܝtN$<@ +kA:E IwZA 6nmQ! /eŢ6i͝h$>†@ HPTV;Iyi.[E[Ll mܜlS38QRQq3k@ 俍X"-( >tE5CjH[Ї7T*v3$9u@ >=4/.G9J%=:kNӣr@ @ z$(;ZYd'g1wo^d&8v.b릉FV4hM IDAT̉CA~%*8[cF۽r{vHtg͙ L@~aO}ߧzk׷\K^9;<ӾۗϾ9}֍ޚ9 @x5z>9?;tN&PMu|EʶM$_p̉x'֜ex98{qӵvF [DEsrp7uƭ;_rb b&q56VWΞTa[6to?}ɇoCGN5tٵ>+,<{rWE|}^Mndt ~~?0Bx:{'=Ϗ0N|ձ Cf^Ѩ y{҉pvJƣ'S#^mt^t}ο{WS拎zL&}ʜ.NNx3m6 ¯ܶ`,Oҝ{(\wmA#j.[#H^-%#.J/}֡4**:8쨓DL٣JGLKs֖L"WvO]t^:ouumuumWeq8>"KwcUT}fyWιzDg0DgZ`OKbo|^w7`Qu{!: UAK$<>*~u ?I Jsuu,(,ѸT`0~>5uuBaTϙ{َ+,9"ٶ/n־Ory'Ō3/<,/?=[7,{>HD=(J6n -Y8wLt/?*v "z@;% zcH ~ mݸN,;;;YZnBQT 5yWP(\]7~MR{c7 oxnǖ AxO>l’scGXZWTݰE ߾ZqV.=޺ME%K O:gˋ%Wo6ފP]kҲ-;oH]4533;wg.qF _8w((,sz[gϘ2}D-Sbei_* \x;[: R3/퟈^$;ϳm:#J{O>tws~$H љՠiǍ>"q{Y[Y}wyDxL.;s3q#b|#YOsWHTdەWTSQIu?"|KKCGNꔢW˸8mؼT=kV2 |񂐠k78;9pBs?k^O/^׹H\^ًW{u;x;^naeiq8-vT7{ӆaQo%huZЇ뷔WVUJm;:茴ޡ2zْ3pX6i.tnq/]?a{od7;BJ!;;:Npo}r)+dҕǎB׌^Q]^2T U'KqI[r;[["Kͻ~+gOa;/}x|g~mYx5i: ǫwSN{3jljZNV7FI3珙2k7+igۧ^t-#9G = H6o?aƭ_,]87пQd{w|0u]_ẍ́;yfE{~w΢ Y0oz; ]'f~O WDI)ccѻ#bdr<;[7~M"xoEf4*Dx/o{:F.[|Go/K>1gsF'zۛv#̚>e͓g-P>0grakt5RzDhk\hCxn&psbwm۔>z0nTq1T".^1o~9/njp'^ΚuۮP(Ђ+*վm X'.wQaxB&/_0vOjd\U!J"l1 r~Z:T|7rr==:LJ_8*jQuM( "ED {s Şih[cȠNs=;XYZoN*.-HwxJFBKf43>I JIa 0y؟~;R١#'cՉP~$[*%&EC**Z[E.ATWn\8p<_ `PpY ܥѱ D`b2J6F+eJv3A,dCGOj3^JYVwm(g"8+(3JR'߻DS-롣2~Z:e I;+c_O}AWZf3q~>؈-Qsv3!>vl̨#b=~DE{VtϳseD9ܒ2//\\X\ ^d2joGqUR_`֑7y㥟>IPziz^iv,KsOPx{z0l8DGY鏳:ի!yA۱ff47_=$3PTRWVGԩ1s++"dxRfb US{mu2|{W/[O?~DfKޙfH_CFG<{o\7 ,(*A'=lP*9yn%a頷 z)Tf7zt_fgÖ_yE %3DYE\hӃG~kÓH,,=He2-T*-#jF3Y{OF4 yJ,%DvSXpY3=)*ЅWϟfpPԇ΋aT*rۮeRB1RT Udp9 Eؖ\b;mdŖ kѥ. x,k:ʠ5o3NbmT*A[4PB*bmu..-6Xx_NFk/:7? K64J**B TU[t:7Rk7n1Rr y^P Yh#133;ϻB4H F"h;Z&_i]i훍X"0)Tji\ J~unwS _Ap+bT׍9p_>f$RR|Q9r*cBFZ-ڷVXS:Aol[py Š/*ha#q÷|dMٺz#bp|m;E D T ֖ǟ<BYy/g˴i{h*ihxT͢w^2"G5Ȼ֖żzwŒ"O 4km@ r\3rޞmx0f8}R(T fF46u֞Ы+:+V,ܳoؐAw.)->vP3+hT s3͛ q4sM͜`0¾ٵѓ,Dccc"|=gm.9" Ł>͗ KSi0 _R)ȠǙKʌFp.S"B1ET*Fr_ӞFU ʖ68s|gG= zַKm6_z"[ xQ)TV \7c 7j[hHʽ#hPڊ:υzsZQ1CY 7 OD&V/_0ÉN# ud=/(ɥZvv4S\Z& G8 եJxeJ++]JbX7@aNǎqfdDH <$x(<7u8ssǙ%vV-]_}WOA'!hZTF"'uĸzNVT/ ^noY"ҽݼh<7\3$_`Lb&a(Zñ #JmF ֬d /)5˝revN/O=΍-eUUTF>~ C־wc"׃GIS=t HB]W'_K o%9zOR 9d2ǮJ[5+?]ayeպO[:Ro XYZ|nQX8wXYZ47sz3-㱑FE()-v3؁e%/ܼ^nE[I)~ŝ $m@0h@hn?~egZkKXzV"vkW/_ʟ֛3Tj&~wM8\.hpƭ/6o鏌LַKm6~?΢ykfN+Zavz{.fY~˓)7]7 r谫d<֮qp$O9xE14$&߳c2۳JffܳJzJZdTTܸ&r]WFV߼OD"UUv= z;M6oo%fCcㆭ;VO?.ZaWA{ouOlsE%oтwv|8|V8+;w`Wg}4:=Z,W\JğzAֽ?y\W~{MƦۉwFE&ʎ:}x<~{UJoENPP6 337^#$(pɂ9kmkA:`-Ja-_qA8[7/*EO_akqi8ʙ3,5y1{OuoNiNCOۙ/I$X %2?%7{C jH$`Hdr[ /7$sOW"PД_,#c<+rreస",noҧϋDb D1?b#c"t(kbʂNhppJD[;L@LSNlt Sn{hF"I2Z,N:{$' xoea6VɩxA~KM]#@* {yqisAB T&bؤ=.7+ˆiia{/=Ejˤ pq@ܮX癓Í<>~hHwQYU`ia&H%RET*xzNR劆&æ͛R%ՈAv:tҜB&uv~đ,uwe@^;1t\z54w/DH$]NB = H`0Wk+ @H&$)Lً ҌFդu=I$MdBVH$f*5! m^p8H薜!HKIΜw'r IDATbF:1S;2r԰7(p)J.WPd `[d˴),m;@ k9mP*i,b4H6N@ "LkenmI!F 86vH<O$҃H$R,DU9ڱ-Bͤ1f]648KB>.>KmheؘQ2)*,Ν@ LkWlM=A=\6VL\.l@HC hz[/4w%RوDH>e5$aXtim?~Hd;=<@@(ɓR9ٳpkH|/=K l~^bjuQY\0?`Z RV,,KP嵤e&}eM= %eUƒj$9QϊʞUZQsZs=)}̚zvM=[Z ȿӲN))ּ HqN;JUSϮyێd@ =u@%<}^R@ Gu@Bx=@ qL@ h@ 1-?/7mYd2Q J*J+ki"B En~I1m5A D 42f/@ݖj qwuJ6b@-vLk+:93'?nHT}cUӂA 1Q$4ߢL: @zs3jDH%"--̂dʪ@D qr , ^CPDL.mL&WLI{UjuQYeQY%H |[jpX/a0t/CI ;ˍnm6V~^ccy9uyeTj5/pqШ& Cz2\*j+U*BiNhW_*"RL;H  @z fY~pudXed?$?xь>I^M&C ΓWd/5'e{{zv3 WTׅ{ybF(g9aAU,a7q1WAqy~!,p8埛0B!@VCD7^TjLYӑ+7y5BQeV@^z$Wu2s._w'YUmC-mǩݾ^\OJ5?_ He̚FM`'EeU:I\l:\ A iY'ڔW9򭻽/ 15Y wTTZj:ǽ3'dސ@zӵN fStm`۱9>"Ŀ :@ Y:t !t N7|@x5Lb@ ;@ nL&=yq 3o͘2ub`Y4ot2,WЕ: Lvvz{znOv 请/ٺx{zv_~;s#5ueoG!'sEOwW#~ֱKu+N8wcF6IPzAj7_aɠ Du/ƣظ<ſvW7z+7JKMGOƘ:|}mH,pNjm(!?S|~KN wƷ16z50>6&81^kGB^ŭnљ*MP_}YPQQ>?a_~{wn?>h_qo-Z>o*&ng|Bnؚ'O{kkD7gg~n.mG4qy|Tb)1;!._nӶí,-Μ<$+329)J՛uqI-J%5uTPB+K *Z\ZM;yU 8z5ES .Z e^``Tĥǚ9k7}eD"zZJS(KWoF /Œ1 M;| ?u3OPrV2~[RL&x^NF dpq)¹iGxm]^QɠW/_5~;(dHfZ Y:"KL(jQ(RX_$ѨP06z5X9 [.QjЙxqKiS3V+k~j_XhinNR;/@xvgw͝5c 4ҕ45wL$eI$RB/!HW%$^Cw5睸8;:n2w~n?[P(t3#J4 4Rb9ͬPBҨ@4覌!ѫʑH43ش35hRS[d2cZMwEcr8n7/m~3ApضH^_}̘?{vHIYŊ nZ*+ps=wJb=;[D2r@`ZN‚YLƹS_9wB.>GT`1,y[YE;z֞8UT:9:HDMJC ^QQ+ztg=JX^Qm,-M:S( # fUa!O[qy|VM,|73k["O%?u&~ ?# ̈́7ObD"qwRu:/3M'",&K8 nP'G/,dޜRZn@&cΜv1q^E}o)r}ian|cN> ,(lWoڢB@^ju@ X'@ @ I:N!ǀs'@L h@ 1-Lbe=2id_p?# !~^v,:܂@ HaIhE2_PH ST&@ 俁)2 ^m-N& ZEbLx@z;pA~3sm@+ 5*n @ A/Y'RdFuc&> GD(U5*|<\VK?@3ڒB&6aqX7SZ"U_K@ 70-뤦 gƦ _O'ۺz-@tb@ &iY' ^+JZ-r _a8-3n:@ 俊iY'ڔWWkmg<@zS<@ 3:@ bZp"@ NR@+s'@L h@ H!߾`[n[n'ˤݝr.FYWN 8)kDNB$?>>ffv lM!n}$$0X>]AH;{)暦o$!QHS>?8P&&HLw sϜ]Ri1Z6YOw>s{Ͼ$茄EpLwKiBhVfc#;h(J\Ѱ8U4-g͂ eI 1iL:zbmQߔIwHFϫ @$=.VVOnd}@&Nx| dDHA |{ܸģ_ONa,f޶Er8,>||)me+4KZA}|ǯsUϪXbxM GoEJhŲ[g.zT4a77>}]4~D#]V*^^s?̝:qqL=sv1o~iY kO$xc 54~%MR?q=4ɔ BP2lp"xk?4?Ùlr CmCy~4?:L#q4Kڦ}&hX=jAÜn]g{ɤ]3w)kӱXLՌ3?v),dX<)yp!2@*Z0,]|12ѽ< Pʕ 66#KEҢ¢Bx!m'Q 743i;x9b09)OG/ VY>V+;?95еV@\ab@Ci=iɮb6w<BRTc|5oLq+p8o :L:Bzwf+.&SٳqTΦV2՟q:qxLfˋ4u]е$]oi'ܷ&t*Iu&F>{?v>dӛ<ߵD"#b"H\*JtuŝSI|v˜ǦD%MG)[H]O(;c}ܘzgID"aBRn> O'0ߌѦ9~׀ʆR0)*y ?. OI'N~K}J IDATKCs|CQ}, R @5dRYbij!3|K7Yu;X{_{tⱾ3J$nXb ^X7wqAdyz=;o|n73>#*$o* /y)`U`}E܋<4KS7L'SHR464I^3D&)O=Y{a=H  e p`žҼ2 zExwjK 3m4[7A>/Pn]Ry Йt=#Vc!#ܮwy6;"2h Xz½+Ge23jlhוA/\ՓV%Wd8n@GU31mT̏b_1̖5 )/(qg91 .\>.%ӊމ=&#  P>NbAA;Ay?ۃb坵&o`bk:~KcuA>.죋Jҋ}w ߛGʫV=f }\GۘJ[8h}L"}a_y؇D%GE;נшƘ9Z#Q>MFw0 Nn_w^ly*؈ʆRa7t om«Ƶo tDځ?& tR+sϭ<0Hxm{.{Pz-aXۮIxXv}up;=[$U#r6Z;txY֛.mݧ{dEvY)o ~*y{;+LU2<1>KN>]& T}p¯Sܿ6ZӚ#`'?ʹ_ [L[2cߩ.32P[Yz# 25hcJU^EaR*!Q9ʆRi>+s,vmBu#f(ޞs[lM)άQ@"`ŚR .l@3K7q&&bIYסL̊yqw$ޘ];,F̨֡Gr9U+q.߻o'D =p6kut2{F,>[ZwB;@sdCXYe8pM^ ʊ}{PSxz>s`ˈIO+sѩ{ |5;i6_3's1:QRXB~n&;G1/̻vLD ^^{/#\Gvމ:*QP:ZQbF-VTdIeɢ{N{bj(Hhv^8ڳ>;8~O\/!ޡǹy~+GDGg#N3=C]1јY$ iQL3VNܗKu.y`~!g]]X]sN.nuwB}{v߬ܒYeƸoפ%,ZV5lnlmްbEvBh`#f56aV*.36U>a.דiGSs]G}*|fjJ]BCux:T;{'fz );q<03ԹcLϺsq`Y(N//аNt:L*IeW܃=މ/rqV~E3Go\&+?ݩ!5PrxyZuT,hq3` JG 31 Wy ;:{ȒdEm=_d:Yf%("hF \{(_Lmr2-lAMCKHSxeo&d+%]y*BSN~}"ik¤(elc2`Ϋ'y&'׏OTz' CifrI؄_&W0&7ružS=n 2(=9l88P+#H;P$*~+BHA'#](B~N>.Ggq9O9}m8|bjOׅ*BʠN7X((mNejk ep׳ˏWn AᚮR .ND}ӿM+/>PҢ.٥ðGuE16ND EQ/jt_œIegpWގ{۰ϓدo%ɤ <)\iוݭ&zO/2D$IMlMdVT@zd~fN c¥ƀ͗RZ_algOU13)j`%VWX$IJ5>g=ίnؼF\Wobc513~U7QG86y6xB!ҩvI$ Ņ&QiOL*˾oGğ7@IŒ $*)`Ǵk/*k9؂j>&_oDZuxt"PLH&ʤ2. U^ЕiW$="%a&NnW7ޓ']~-8xw1i{p|?qB¯=wb1ᗠۿ_+N-$QHrMͰZa`ԛϽo8wpvaUYF 3`w.C\93Ǹ=BW IeigzUl,׷04F];u61}v1i(u _D=?~˄<Ϲ>E/p D%9z;|'I<=.? ª/Ks8]KB o i?Y:|r2< L49A~|NzdC/$Vp ~;q3hI;ɥ[ɱF@F gn|/El;!5+~}iK N+({gr~vz'8y+IrfD?b 1[и"Mi"fx G"#"z']v}"C+?1s0u_ce],Paߏp Dq'b?NTֱNΧ^렲T&{O3_%^[RQ#QIۃ͈$"2̘t)ްUc;vįş~|)(DoA2C4 h;AA!=  NA.Z1+  @c' h;A˦vv]63gFp[a7z3)NLMM"__!ӧ^:sK~^l ۺŐwa?쯦^x:V,!F= ;yx[wצpNP>:*SY_u <ϝ<};i5bh}Q7._ӪtMlC5\>2& q³wjuMD;IdpP(JU&%22,;hr86p^߶UFr'V@>.]SE.]gM(@&Lveǖ &.Y8hb𬵛}`V5]TW]B3Sӟб1Ӫ6]z-i~բeߛw03sZ|^n=jxQkϝ<4rP{p)&lǿ{~722lW.7kNF\?v;CONB!]],::J pqղ j޾ؾJg],:kቐЁ[,[S~qp*ʥݯDB!ᔗrص*ֺ JYh=ob.&@biUVv.8+;v΅qrm;KhfbCiɳM]ߎXwifs6HB"]zf|sThaad2֝N] eQ ny|ޣ\q.>"3¢CsZÆ~Q=fs8@hia`/==fGNm]3SZRФ EqNVQfFQfFi^navfQaMyIứ4qGqEyUPUGKu;x t/h{NԽKLF+ :]]Bp>yQm:L|_GGgi YK:Z2b2utO7lV@&S'ryzf2@P1ACc2($F7`ib=v԰V.2( yc#3N׃ D"L$Q(4UbiwޞfV:5c7"܌cjf?ӨsE 'NX2PW'T 1tLA`2eR)aTWm uo @Z%-bZW`ۆH$Vz-L.ʈ B"&J1n޶ȑw#R(Ɂn\< 2I<@]U Rihוn.&B_3s[wװ ˏ<<{L@p³dHhp峑R1ΦLyr :ZZ׹8UTv66xu~~FA8*.a^~mC#?~=Z%-6¼tC}Jv[wų|% +MMMt:;H.kQw&[7ŧ5w4D/2!(/lts@dnM® h[VdT3/9sSg^L]ggG]]FWg5{th}}*Q( d2?n܎?fU'#CNv3_8wU]\Z:g4:vp#CqJ"̘':mi*.aDT㠁:t3RJJ۱RWS_#057{.uiN>yzQ}J ų~7?E3ɹߝcM ŚXrrvڻyO'ھn {`zϞqKnN=p֭Yya@a/_ۻ/&j _MQ|!T{?ɮ;YZ9܋W mI\WwF6gFdȘgE^vָliNK~۵719:t8}!,r*0dyWqTW]xy/[(19u]~ePeE{"h¹~CWW>wNaA4uxD{{' |{'Uj!"`?a+b@AA>WBor,V1m  WU/`PUQZ2We;<{AAIiKPheZ AA4 @ /,ikZ2v  gSW/A(T2;AADCuKp  NA.w  vAAA  Xۻ$ |;z'X  Y3ȟ_u$ڻ\ |y;U?g_ܮY  qByy5 'J.m" GAAD쑦k,G.ML@;}& Oww4~{A4]AAD  NA.w|D{w_`zdmώuMDrxΔw÷̶6vpv̜dyw95#>=wuaEouT nݺ,=Y9dC>jn ;ZtZ #3/Z$F-k~zK3<~P7\R'M[9wO3!BCIR6 r[yn\.oIQ"fl.CJEbI5u=^,h-;AW\BQƆxHߞ%evMDBH.a5%RˋH$HG\KH22`-1)`УgQ|UT&ɚ/gTVq>8*$ JsOJL8OB͌G|ޖ? >VCDEdw "0n}SHĴ3nֿSudž\ӨԜ %f˹5}{݈CIbSAc-MR鳔g.K$xQJRws엟?{v2n>_ /:.2aq"0{ݻ5_)hT^Fy NWTExyR"bbj񬌜QO L :/8r UNHR&wsqdqnEK2/^}ܺrjyv-=Fn'26K2TV^ɎүWբ2НFdd:/XsXr<)5șrLfNW^Y}#r.#x."[)pH$/&~k:]{:8̸цF,.={-*0Գ7(,)?#JՈL&JeyJr -̌ Xgy𴧫ciE1c}=\l2YBJƭRl) ڢxsbaIaUfG[*ߐfRd@5y #|dQ#>M 4*e϶YU,X̞8;֋] ;O 6IDATˠ?fgm*? Zq"_2;Am~u~#}Άjκӎ=GuVt81pߕr?Cutha7s tuu̝~NNv:$YLAE/)|Ţr_+('cW^y)?n٭CvҸa yU\6īߢ:HT&;xes3s;;ɤ1S=\[1/xo;0`_RoO nG?gG?8r0>*Y9NM@P\ʫy*6wFYSg6-/jy/hMdmoe 4"Sg9ZPs&%773YO3ルYXTh}3'[l:-?-02d鬼~;w24h݉|ЬXmU(½=z_ D"[ѽ\M{c|H?n{OזdASm3sʌKȠQ YصyBʫ4w#؏qkyx!;t޻ۥwuB6Z}%/@.kd8-iթF[Vڡ_wŗUDsawuԡ%Ȥ&,=T%9gSn&V;O$;L*hiC E%-̮id2Lz/!UiL&}5RZ>+pӷ~X85=q <0Q".7b 95 90Md}ID|n!ԝ]~G"?OK[eέ2,+ E6ޫszf^FNL&P DEdhy?i/BntچU_Bf2tL/Hx ZFɓ4>=r ~Z$1 ܉L$$ . ,'1350LNVRU$C};N}+BXLb í!ſ"I1CS3**&HBSQZ*BQ S,^5*k݄QR2,$>"][F Y݂ZFĽd|T42'VK$J`cy{ܕSWA2Db@Ŵءo7'E.op(ZVP'T|֋"(2Jaskp6 -D5Okg1:Ps&skbD1mӨw֡b⢤^5\.gBȗ N#wH D"1_PWeM56jr_7 =~IL1CE"ѦzDoE3nz㤴9'[ß1ygJ:VZQ%)dW VhE%e2P/F0O3SF5aceaJݣ뻣AI$oO[k!O_>'x⫽KDay~G?M?GN3`1GzũjI#g>S(~3uB@@$s>ze_.bVJEAT*af6-)r])D" d-+D H Bt rw270J!wb׊2GxS),&w@Y-I&YÈDC`i`T߶L#}DE'ˢR5JMe|~k;I?r' WnF͞:W7:a}fvk8>D[KV*UZFN{lNG6yF+g512EyO j{Zſ/ 8lji=\Nr?>q򄑝,L PW"N%R,.!9nܻiI#' Y+MQ/E"{[H"w9`yđ2wL4 >:i֟Pʰ[`= %-9pOӲ2r^6?3U /R2r߷ Wy(X&%f}ܢk^m5xѧX"fsRg?~?՝zQiy[!ɮEgd|ܚgd 9WVn*9GN͜2CUqD*g3k|?!(.;6s J]?}͛>18"FEi&pxv~c56Uhw7'?itk7@}w=RRV.: Е)wʻ/"(Cc'|0 W  84+A%ApNAA>Q9c9ˣIENDB`spyder-line-profiler-0.4.1/requirements/000077500000000000000000000000001476356134600203315ustar00rootroot00000000000000spyder-line-profiler-0.4.1/requirements/conda.txt000066400000000000000000000000331476356134600221520ustar00rootroot00000000000000line_profiler spyder>=6,<7 spyder-line-profiler-0.4.1/requirements/tests.txt000066400000000000000000000000211476356134600222250ustar00rootroot00000000000000pytest pytest-qt spyder-line-profiler-0.4.1/setup.py000066400000000000000000000067321476356134600173300ustar00rootroot00000000000000# -*- coding: utf-8 -*- # # ----------------------------------------------------------------------------- # Copyright (c) 2015- Spyder Project Contributors # # Released under the terms of the MIT License # (see LICENSE.txt in the project root directory for details) # ----------------------------------------------------------------------------- """ Setup script for spyder_line_profiler """ from setuptools import setup, find_packages import os import os.path as osp def get_version(): """Get version from source file""" import codecs with codecs.open("spyder_line_profiler/__init__.py", encoding="utf-8") as f: lines = f.read().splitlines() for l in lines: if "__version__" in l: version = l.split("=")[1].strip() version = version.replace("'", '').replace('"', '') return version def get_package_data(name, extlist): """Return data files for package *name* with extensions in *extlist*""" flist = [] # Workaround to replace os.path.relpath (not available until Python 2.6): offset = len(name) + len(os.pathsep) for dirpath, _dirnames, filenames in os.walk(name): for fname in filenames: if not fname.startswith('.') and osp.splitext(fname)[1] in extlist: flist.append(osp.join(dirpath, fname)[offset:]) return flist # Requirements REQUIREMENTS = ['line_profiler', 'qtawesome', 'spyder>=6,<7'] EXTLIST = ['.jpg', '.png', '.json', '.mo', '.ini'] LIBNAME = 'spyder_line_profiler' LONG_DESCRIPTION = """ This is a plugin for the Spyder IDE that integrates the Python line profiler. It allows you to see the time spent in every line. Usage ----- Add a ``@profile`` decorator to the functions that you wish to profile then press Shift+F10 (line profiler default) to run the profiler on the current script, or go to ``Run > Run line profiler``. The results will be shown in a dockwidget, grouped by function. Lines with a stronger color take more time to run. .. image: https://raw.githubusercontent.com/spyder-ide/spyder-line-profiler/master/img_src/screenshot_profiler.png """ setup( name=LIBNAME, version=get_version(), packages=find_packages(), package_data={LIBNAME: get_package_data(LIBNAME, EXTLIST)}, keywords=["Qt PyQt5 PySide2 spyder plugins spyplugins line_profiler profiler"], install_requires=REQUIREMENTS, url='https://github.com/spyder-ide/spyder-line-profiler', license='MIT', python_requires='>= 3.8', entry_points={ "spyder.plugins": [ "spyder_line_profiler = spyder_line_profiler.spyder.plugin:SpyderLineProfiler" ], }, author="Spyder Project Contributors", description='Plugin for the Spyder IDE that integrates the Python line profiler.', long_description=LONG_DESCRIPTION, classifiers=[ 'Development Status :: 4 - Beta', 'Environment :: X11 Applications :: Qt', 'Environment :: Win32 (MS Windows)', 'Intended Audience :: Developers', 'License :: OSI Approved :: MIT License', 'Operating System :: OS Independent', 'Programming Language :: Python :: 3', 'Programming Language :: Python :: 3.8', 'Programming Language :: Python :: 3.9', 'Programming Language :: Python :: 3.10', 'Programming Language :: Python :: 3.11', 'Programming Language :: Python :: 3.12', 'Topic :: Software Development', 'Topic :: Text Editors :: Integrated Development Environments (IDE)']) spyder-line-profiler-0.4.1/spyder_line_profiler/000077500000000000000000000000001476356134600220255ustar00rootroot00000000000000spyder-line-profiler-0.4.1/spyder_line_profiler/__init__.py000066400000000000000000000006201476356134600241340ustar00rootroot00000000000000# -*- coding: utf-8 -*- # # ----------------------------------------------------------------------------- # Copyright (c) 2013- Spyder Project Contributors # # Released under the terms of the MIT License # (see LICENSE.txt in the project root directory for details) # ----------------------------------------------------------------------------- """ Spyder Line Profiler. """ __version__ = "0.4.1" spyder-line-profiler-0.4.1/spyder_line_profiler/example/000077500000000000000000000000001476356134600234605ustar00rootroot00000000000000spyder-line-profiler-0.4.1/spyder_line_profiler/example/__init__.py000066400000000000000000000000001476356134600255570ustar00rootroot00000000000000spyder-line-profiler-0.4.1/spyder_line_profiler/example/profiling_test_script.py000066400000000000000000000023341476356134600304500ustar00rootroot00000000000000#!/usr/bin/python # -*- coding: utf-8 -*- # # ----------------------------------------------------------------------------- # Copyright (c) 2013- Spyder Project Contributors # # Released under the terms of the MIT License # (see LICENSE.txt in the project root directory for details) # ----------------------------------------------------------------------------- from __future__ import ( print_function, division, unicode_literals, absolute_import) import subdir.profiling_test_script2 as script2 @profile def fact(n): result = 1 for i in range(2, n // 4): result *= i result = 1 # This is a comment for i in range(2, n // 16): result *= i result = 1 if False: # This won't be run raise RuntimeError("What are you doing here ???") for i in range(2, n + 1): result *= i return result # This is after the end of the function. if False: # This won't be run raise RuntimeError("It's getting bad.") @profile def sum_(n): result = 0 for i in range(1, n + 1): result += i return result if __name__ == "__main__": print(fact(120)) print(sum_(120)) print(script2.fact2(120)) print(script2.sum2(120)) spyder-line-profiler-0.4.1/spyder_line_profiler/example/subdir/000077500000000000000000000000001476356134600247505ustar00rootroot00000000000000spyder-line-profiler-0.4.1/spyder_line_profiler/example/subdir/__init__.py000066400000000000000000000000001476356134600270470ustar00rootroot00000000000000spyder-line-profiler-0.4.1/spyder_line_profiler/example/subdir/profiling_test_script2.py000066400000000000000000000013441476356134600320220ustar00rootroot00000000000000#!/usr/bin/python # -*- coding: utf-8 -*- # # ----------------------------------------------------------------------------- # Copyright (c) 2013- Spyder Project Contributors # # Released under the terms of the MIT License # (see LICENSE.txt in the project root directory for details) # ----------------------------------------------------------------------------- from __future__ import ( print_function, division, unicode_literals, absolute_import) @profile def fact2(n): result = 1 for i in range(2, n + 1): result *= i * 2 return result def sum2(n): result = 0 for i in range(1, n + 1): result += i * 2 return result if __name__ == "__main__": print(fact2(120)) print(sum2(120)) spyder-line-profiler-0.4.1/spyder_line_profiler/spyder/000077500000000000000000000000001476356134600233335ustar00rootroot00000000000000spyder-line-profiler-0.4.1/spyder_line_profiler/spyder/__init__.py000066400000000000000000000005731476356134600254510ustar00rootroot00000000000000# -*- coding: utf-8 -*- # # ----------------------------------------------------------------------------- # Copyright (c) 2013- Spyder Project Contributors # # Released under the terms of the MIT License # (see LICENSE.txt in the project root directory for details) # -----------------------------------------------------------------------------""" """ Spyder Line Profiler """ spyder-line-profiler-0.4.1/spyder_line_profiler/spyder/config.py000066400000000000000000000021161476356134600251520ustar00rootroot00000000000000# -*- coding: utf-8 -*- # # ----------------------------------------------------------------------------- # Copyright (c) 2022- Spyder Project Contributors # # Released under the terms of the MIT License # (see LICENSE.txt in the project root directory for details) # ----------------------------------------------------------------------------- """Spyder line-profiler default configuration.""" CONF_SECTION = 'spyder_line_profiler' CONF_DEFAULTS = [ (CONF_SECTION, { 'use_colors': True, } ), ('shortcuts', { 'spyder_line_profiler/Run file in spyder_line_profiler': 'Shift+F10', } ) ] # IMPORTANT NOTES: # 1. If you want to *change* the default value of a current option, you need to # do a MINOR update in config version, e.g. from 1.0.0 to 1.1.0 # 2. If you want to *remove* options that are no longer needed in our codebase, # or if you want to *rename* options, then you need to do a MAJOR update in # version, e.g. from 1.0.0 to 2.0.0 # 3. You don't need to touch this value if you're just adding a new option CONF_VERSION = '2.0.0' spyder-line-profiler-0.4.1/spyder_line_profiler/spyder/confpage.py000066400000000000000000000044071476356134600254740ustar00rootroot00000000000000# -*- coding: utf-8 -*- # # ----------------------------------------------------------------------------- # Copyright (c) 2013- Spyder Project Contributors # # Released under the terms of the MIT License # (see LICENSE.txt in the project root directory for details) # ----------------------------------------------------------------------------- """ Spyder Line Profiler 5 Preferences Page. """ # Third party imports from qtpy.QtCore import Qt from qtpy.QtWidgets import QGroupBox, QLabel, QVBoxLayout from spyder.api.preferences import PluginConfigPage from spyder.api.translations import get_translation # Local imports from .widgets import SpyderLineProfilerWidget # Localization _ = get_translation("spyder_line_profiler.spyder") class SpyderLineProfilerConfigPage(PluginConfigPage): # --- PluginConfigPage API # ------------------------------------------------------------------------ def setup_page(self): settings_group = QGroupBox(_("Settings")) use_color_box = self.create_checkbox( _("Use deterministic colors to differentiate functions"), 'use_colors', default=True) results_group = QGroupBox(_("Results")) results_label1 = QLabel(_("Line profiler plugin results " "(the output of kernprof.py)\n" "are stored here:")) results_label1.setWordWrap(True) # Warning: do not try to regroup the following QLabel contents with # widgets above -- this string was isolated here in a single QLabel # on purpose: to fix Issue 863 of Profiler plugin results_label2 = QLabel(SpyderLineProfilerWidget.DATAPATH) results_label2.setTextInteractionFlags(Qt.TextSelectableByMouse) results_label2.setWordWrap(True) settings_layout = QVBoxLayout() settings_layout.addWidget(use_color_box) settings_group.setLayout(settings_layout) results_layout = QVBoxLayout() results_layout.addWidget(results_label1) results_layout.addWidget(results_label2) results_group.setLayout(results_layout) vlayout = QVBoxLayout() vlayout.addWidget(settings_group) vlayout.addWidget(results_group) vlayout.addStretch(1) self.setLayout(vlayout)spyder-line-profiler-0.4.1/spyder_line_profiler/spyder/plugin.py000066400000000000000000000126601476356134600252100ustar00rootroot00000000000000# -*- coding: utf-8 -*- # # ----------------------------------------------------------------------------- # Copyright (c) 2013- Spyder Project Contributors # # Released under the terms of the MIT License # (see LICENSE.txt in the project root directory for details) # ----------------------------------------------------------------------------- """ Spyder Line Profiler Plugin. """ # Third-party imports import qtawesome as qta from qtpy.QtCore import Signal # Spyder imports from spyder.api.plugins import Plugins, SpyderDockablePlugin from spyder.api.translations import get_translation from spyder.api.plugin_registration.decorators import ( on_plugin_available, on_plugin_teardown) from spyder.plugins.mainmenu.api import ApplicationMenus, RunMenuSections from spyder.plugins.profiler.widgets.run_conf import ( ProfilerPyConfigurationGroup) from spyder.plugins.run.api import RunContext, RunExecutor, run_execute from spyder.utils.icon_manager import ima # Local imports from spyder_line_profiler.spyder.config import ( CONF_SECTION, CONF_DEFAULTS, CONF_VERSION) from spyder_line_profiler.spyder.confpage import SpyderLineProfilerConfigPage from spyder_line_profiler.spyder.widgets import ( SpyderLineProfilerWidget, is_lineprofiler_installed) # Localization _ = get_translation("spyder_line_profiler.spyder") class SpyderLineProfiler(SpyderDockablePlugin, RunExecutor): """ Spyder Line Profiler plugin for Spyder 5. """ NAME = "spyder_line_profiler" REQUIRES = [Plugins.Preferences, Plugins.Editor, Plugins.Run] OPTIONAL = [] TABIFY = [Plugins.Help] WIDGET_CLASS = SpyderLineProfilerWidget CONF_SECTION = CONF_SECTION CONF_DEFAULTS = CONF_DEFAULTS CONF_VERSION = CONF_VERSION CONF_WIDGET_CLASS = SpyderLineProfilerConfigPage CONF_FILE = True # --- Signals sig_finished = Signal() """This signal is emitted to inform the profile profiling has finished.""" # --- SpyderDockablePlugin API # ------------------------------------------------------------------------ @staticmethod def get_name(): return _("Line Profiler") @staticmethod def get_description(): return _("Line profiler display for Spyder") @classmethod def get_icon(cls): return qta.icon('mdi.speedometer', color=ima.MAIN_FG_COLOR) def on_initialize(self): self.widget = self.get_widget() self.widget.sig_finished.connect(self.sig_finished) self.executor_configuration = [ { 'input_extension': 'py', 'context': { 'name': 'File' }, 'output_formats': [], 'configuration_widget': ProfilerPyConfigurationGroup, 'requires_cwd': True, 'priority': 7 } ] @on_plugin_available(plugin=Plugins.Run) def on_run_available(self): run = self.get_plugin(Plugins.Run) run.register_executor_configuration(self, self.executor_configuration) if is_lineprofiler_installed(): run.create_run_in_executor_button( RunContext.File, self.NAME, text=_('Run line profiler'), tip=_('Run line profiler'), icon=self.get_icon(), shortcut_context='spyder_line_profiler', register_shortcut=True, add_to_menu={ "menu": ApplicationMenus.Run, "section": RunMenuSections.RunInExecutors } ) @on_plugin_available(plugin=Plugins.Editor) def on_editor_available(self): widget = self.get_widget() editor = self.get_plugin(Plugins.Editor) widget.sig_edit_goto_requested.connect(editor.load) @on_plugin_available(plugin=Plugins.Preferences) def on_preferences_available(self): preferences = self.get_plugin(Plugins.Preferences) preferences.register_plugin_preferences(self) @on_plugin_teardown(plugin=Plugins.Run) def on_run_teardown(self): run = self.get_plugin(Plugins.Run) run.deregister_executor_configuration( self, self.executor_configuration) run.destroy_run_in_executor_button( RunContext.File, self.NAME) @on_plugin_teardown(plugin=Plugins.Preferences) def on_preferences_teardown(self): preferences = self.get_plugin(Plugins.Preferences) preferences.deregister_plugin_preferences(self) @on_plugin_teardown(plugin=Plugins.Editor) def on_editor_teardown(self): widget = self.get_widget() editor = self.get_plugin(Plugins.Editor) widget.sig_edit_goto_requested.disconnect(editor.load) def check_compatibility(self): valid = True message = "" # Note: Remember to use _("") to localize the string return valid, message def on_close(self, cancellable=True): return True # --- Public API # ------------------------------------------------------------------------ @run_execute(context=RunContext.File) def run_file(self, input, conf): self.switch_to_plugin() exec_params = conf['params'] cwd_opts = exec_params['working_dir'] params = exec_params['executor_params'] run_input = input['run_input'] filename = run_input['path'] wdir = cwd_opts['path'] args = params['args'] self.get_widget().analyze(filename, wdir=wdir, args=args) spyder-line-profiler-0.4.1/spyder_line_profiler/spyder/widgets.py000066400000000000000000000707261476356134600253670ustar00rootroot00000000000000# -*- coding: utf-8 -*- # # ----------------------------------------------------------------------------- # Copyright (c) 2013- Spyder Project Contributors # # Released under the terms of the MIT License # (see LICENSE.txt in the project root directory for details) # ----------------------------------------------------------------------------- """ Spyder Line Profiler Main Widget. """ # Standard library imports import inspect import linecache import logging import os import os.path as osp import pickle import re import time from datetime import datetime # Third party imports from qtpy.QtGui import QBrush, QColor, QFont from qtpy.QtCore import (QByteArray, QProcess, Qt, QProcessEnvironment, Signal, QTimer) from qtpy.QtWidgets import (QMessageBox, QVBoxLayout, QLabel, QTreeWidget, QTreeWidgetItem, QApplication) from qtpy.compat import getopenfilename, getsavefilename # Spyder imports from spyder.api.config.decorators import on_conf_change from spyder.api.translations import get_translation from spyder.api.widgets.main_widget import PluginMainWidget from spyder.config.base import get_conf_path from spyder.plugins.variableexplorer.widgets.texteditor import TextEditor from spyder.utils import programs from spyder.utils.misc import getcwd_or_home from spyder.utils.palette import SpyderPalette from spyder.widgets.comboboxes import PythonModulesComboBox # Local imports from spyder_line_profiler.spyder.config import CONF_SECTION # Localization and logging _ = get_translation("spyder") logger = logging.getLogger(__name__) COL_NO = 0 COL_HITS = 1 COL_TIME = 2 COL_PERHIT = 3 COL_PERCENT = 4 COL_LINE = 5 COL_POS = 0 # Position is not displayed but set as Qt.UserRole CODE_NOT_RUN_COLOR = QBrush(QColor.fromRgb(128, 128, 128, 200)) # Cycle to use when coloring lines from different functions COLOR_CYCLE = [ SpyderPalette.GROUP_1, SpyderPalette.GROUP_4, SpyderPalette.GROUP_10, SpyderPalette.GROUP_12, SpyderPalette.GROUP_2, SpyderPalette.GROUP_8, SpyderPalette.GROUP_6] WEBSITE_URL = 'http://pythonhosted.org/line_profiler/' def is_lineprofiler_installed(): """ Check if the program and the library for line_profiler is installed. """ return (programs.is_module_installed('line_profiler') and programs.is_module_installed('kernprof')) class TreeWidgetItem(QTreeWidgetItem): """ An extension of QTreeWidgetItem that replaces the sorting behaviour such that the sorting is not purely by ASCII index but by natural sorting, e.g. multi-digit numbers sorted based on their value instead of individual digits. Taken from https://stackoverflow.com/questions/21030719/sort-a-pyside-qtgui- qtreewidget-by-an-alpha-numeric-column/ """ def __lt__(self, other): """ Compare a widget text entry to another entry. """ column = self.treeWidget().sortColumn() key1 = self.text(column) key2 = other.text(column) return self.natural_sort_key(key1) < self.natural_sort_key(key2) @staticmethod def natural_sort_key(key): """ Natural sorting for both numbers and strings containing numbers. """ regex = r'(\d*\.\d+|\d+)' parts = re.split(regex, key) return tuple((e if i % 2 == 0 else float(e)) for i, e in enumerate(parts)) class SpyderLineProfilerWidgetActions: # Triggers Browse = 'browse_action' Clear = 'clear_action' Collapse = 'collapse_action' Expand = 'expand_action' LoadData = 'load_data_action' Run = 'run_action' SaveData = 'save_data_action' ShowOutput = 'show_output_action' Stop = 'stop_action' class SpyderLineProfilerWidgetMainToolbarSections: Main = 'main_section' ExpandCollaps = 'expand_collaps_section' ShowOutput = 'show_output_section' class SpyderLineProfilerWidgetToolbars: Information = 'information_toolbar' class SpyderLineProfilerWidgetMainToolbarItems: FileCombo = 'file_combo' class SpyderLineProfilerWidgetInformationToolbarSections: Main = 'main_section' class SpyderLineProfilerWidgetInformationToolbarItems: Stretcher1 = 'stretcher_1' Stretcher2 = 'stretcher_2' DateLabel = 'date_label' class SpyderLineProfilerWidget(PluginMainWidget): # PluginMainWidget class constants CONF_SECTION = CONF_SECTION DATAPATH = get_conf_path('lineprofiler.results') VERSION = '0.0.1' redirect_stdio = Signal(bool) sig_finished = Signal() # Signals sig_edit_goto_requested = Signal(str, int, str) """ This signal will request to open a file in a given row and column using a code editor. Parameters ---------- path: str Path to file. row: int Cursor starting row position. word: str Word to select on given row. """ def __init__(self, name=None, plugin=None, parent=None): super().__init__(name, plugin, parent) self.setWindowTitle("Line profiler") # Attributes self._last_wdir = None self._last_args = None self.pythonpath = None self.error_output = None self.output = None self.use_colors = True self.process = None self.started_time = None # Widgets self.filecombo = PythonModulesComboBox( self, id_=SpyderLineProfilerWidgetMainToolbarItems.FileCombo) self.datatree = LineProfilerDataTree(self) self.datelabel = QLabel(self) self.datelabel.ID = SpyderLineProfilerWidgetInformationToolbarItems.DateLabel self.datelabel.setText(_('Please select a file to profile, with ' 'added @profile decorators for functions')) self.timer = QTimer(self) layout = QVBoxLayout() layout.addWidget(self.datatree) self.setLayout(layout) # Signals self.datatree.sig_edit_goto_requested.connect( self.sig_edit_goto_requested) # --- PluginMainWidget API # ------------------------------------------------------------------------ def get_title(self): return _("Line Profiler") def get_focus_widget(self): pass def setup(self): self.start_action = self.create_action( SpyderLineProfilerWidgetActions.Run, text=_("Profile by line"), tip=_("Run line profiler"), icon=self.create_icon('run'), triggered=self.start, ) self.stop_action = self.create_action( SpyderLineProfilerWidgetActions.Stop, text=_("Stop"), tip=_("Stop current profiling"), icon=self.create_icon('stop'), triggered=self.kill_if_running, ) self.browse_action = self.create_action( SpyderLineProfilerWidgetActions.Browse, text=_("Open Script"), tip=_('Select Python script'), icon=self.create_icon('fileopen'), triggered=self.select_file, ) self.log_action = self.create_action( SpyderLineProfilerWidgetActions.ShowOutput, text=_("Show Result"), tip=_("Show program's output"), icon=self.create_icon('log'), triggered=self.show_log, ) self.collapse_action = self.create_action( SpyderLineProfilerWidgetActions.Collapse, text=_("Collaps"), tip=_('Collapse all'), icon=self.create_icon('collapse'), triggered=lambda dD=-1: self.datatree.collapseAll(), ) self.expand_action = self.create_action( SpyderLineProfilerWidgetActions.Expand, text=_("Expand"), tip=_('Expand all'), icon=self.create_icon('expand'), triggered=lambda dD=-1: self.datatree.expandAll(), ) self.save_action = self.create_action( SpyderLineProfilerWidgetActions.SaveData, text=_("Save data"), tip=_('Save line profiling data'), icon=self.create_icon('filesave'), triggered=self.save_data, ) self.clear_action = self.create_action( SpyderLineProfilerWidgetActions.Clear, text=_("Clear output"), tip=_('Clear'), icon=self.create_icon('editdelete'), triggered=self.clear_data, ) self.set_running_state(False) self.start_action.setEnabled(False) self.clear_action.setEnabled(False) self.log_action.setEnabled(False) self.save_action.setEnabled(False) # Main Toolbar toolbar = self.get_main_toolbar() for item in [self.filecombo, self.browse_action, self.start_action, self.stop_action]: self.add_item_to_toolbar( item, toolbar=toolbar, section=SpyderLineProfilerWidgetMainToolbarSections.Main, ) # Secondary Toolbar secondary_toolbar = self.create_toolbar( SpyderLineProfilerWidgetToolbars.Information) for item in [self.collapse_action, self.expand_action, self.create_stretcher( id_=SpyderLineProfilerWidgetInformationToolbarItems.Stretcher1), self.datelabel, self.create_stretcher( id_=SpyderLineProfilerWidgetInformationToolbarItems.Stretcher2), self.log_action, self.save_action, self.clear_action]: self.add_item_to_toolbar( item, toolbar=secondary_toolbar, section=SpyderLineProfilerWidgetInformationToolbarSections.Main, ) if not is_lineprofiler_installed(): for widget in (self.datatree, self.filecombo, self.log_action, self.start_action, self.stop_action, self.browse_action, self.collapse_action, self.expand_action): widget.setDisabled(True) text = _( 'Please install the line_profiler module' ) % WEBSITE_URL self.datelabel.setText(text) self.datelabel.setOpenExternalLinks(True) else: pass def analyze(self, filename=None, wdir=None, args=None, use_colors=True): self.use_colors = use_colors if not is_lineprofiler_installed(): return self.kill_if_running() #index, _data = self.get_data(filename) # FIXME: storing data is not implemented yet if filename is not None: filename = osp.abspath(str(filename)) index = self.filecombo.findText(filename) if index == -1: self.filecombo.addItem(filename) self.filecombo.setCurrentIndex(self.filecombo.count()-1) else: self.filecombo.setCurrentIndex(index) self.filecombo.selected() if self.filecombo.is_valid(): filename = str(self.filecombo.currentText()) if wdir is None: wdir = osp.dirname(filename) self.start(wdir, args) def select_file(self): self.redirect_stdio.emit(False) pwd = getcwd_or_home() filename, _selfilter = getopenfilename( self, _("Select Python script"), pwd, _("Python scripts")+" (*.py ; *.pyw)") self.redirect_stdio.emit(False) if filename: self.analyze(filename) def show_log(self): if self.output: editor = TextEditor(self.output, title=_("Line profiler output"), readonly=True, parent=self) # Call .show() to dynamically resize editor; # see spyder-ide/spyder#12202 editor.show() editor.exec_() def show_errorlog(self): if self.error_output: editor = TextEditor(self.error_output, title=_("Line profiler output"), readonly=True, parent=self) self.datelabel.setText(_('Profiling did not complete (error)')) # Call .show() to dynamically resize editor; # see spyder-ide/spyder#12202 editor.show() editor.exec_() def update_timer(self): elapsed = str(datetime.now() - self.started_time).split(".")[0] self.datelabel.setText(_(f'Profiling, please wait... elapsed: {elapsed}')) def start(self, wdir=None, args=None): filename = str(self.filecombo.currentText()) if wdir in [None, False]: wdir = self._last_wdir if wdir in [None, False]: wdir = osp.dirname(filename) if args is None: args = self._last_args if args is None: args = [] self._last_wdir = wdir self._last_args = args self.datelabel.setText(_('Profiling starting up, please wait...')) self.started_time = datetime.now() self.process = QProcess(self) self.process.setProcessChannelMode(QProcess.SeparateChannels) self.process.setWorkingDirectory(wdir) self.process.readyReadStandardOutput.connect(self.read_output) self.process.readyReadStandardError.connect( lambda: self.read_output(error=True)) self.process.finished.connect(self.finished) proc_env = QProcessEnvironment() for k, v in os.environ.items(): proc_env.insert(k, v) proc_env.remove('PYTHONPATH') if self.pythonpath is not None: logger.debug(f"Pass Pythonpath {self.pythonpath} to process") proc_env.insert('PYTHONPATH', os.pathsep.join(self.pythonpath)) self.process.setProcessEnvironment(proc_env) self.clear_data() self.error_output = '' # Use UTF-8 mode so that profiler writes its output to DATAPATH using # UTF-8 encoding, instead of the ANSI code page on Windows. # See issue spyder-ide/spyder-line-profiler#90 # # UTF-8 mode also changes the encoding of stdin/stdout/stdout which must # be taken into account when using stdandard I/O. p_args = ['-X', 'utf8', '-m', 'kernprof', '-lvb', '-o', self.DATAPATH] if os.name == 'nt': # On Windows, one has to replace backslashes by slashes to avoid # confusion with escape characters (otherwise, for example, '\t' # will be interpreted as a tabulation): p_args.append(osp.normpath(filename).replace(os.sep, '/')) else: p_args.append(filename) if args: p_args.extend(programs.shell_split(args)) executable = self.get_conf('executable', section='main_interpreter') if executable.endswith('spyder.exe'): # py2exe distribution executable = 'python.exe' logger.debug(f'Starting process with {executable=} and {p_args=}') self.process.start(executable, p_args) running = self.process.waitForStarted() self.set_running_state(running) self.timer.timeout.connect(self.update_timer) self.timer.start(1000) if not running: QMessageBox.critical(self, _("Error"), _("Process failed to start")) def set_running_state(self, state=True): self.start_action.setEnabled(not state) self.stop_action.setEnabled(state) def read_output(self, error=False): if error: self.process.setReadChannel(QProcess.StandardError) else: self.process.setReadChannel(QProcess.StandardOutput) qba = QByteArray() while self.process.bytesAvailable(): if error: qba += self.process.readAllStandardError() else: qba += self.process.readAllStandardOutput() # encoding: Python process is started with UTF-8 mode text = str(qba.data(), encoding="utf-8") if error: self.error_output += text else: self.output += text def finished(self): self.timer.stop() self.set_running_state(False) self.output = self.error_output + self.output if not self.output == 'aborted': elapsed = str(datetime.now() - self.started_time).split(".")[0] self.show_data(justanalyzed=True) self.datelabel.setText(_(f'Profiling finished after {elapsed}')) self.show_errorlog() # If errors occurred, show them. self.sig_finished.emit() def kill_if_running(self): self.datelabel.setText(_('Profiling aborted.')) if self.process is not None: if self.process.state() == QProcess.Running: self.process.kill() self.output = 'aborted' self.process.waitForFinished() @on_conf_change(section='pythonpath_manager', option='spyder_pythonpath') def _update_pythonpath(self, value): self.pythonpath = value def clear_data(self): self.datatree.clear() self.clear_action.setEnabled(False) self.log_action.setEnabled(False) self.save_action.setEnabled(False) self.output = '' def show_data(self, justanalyzed=False): if not justanalyzed: self.clear_data() output_exists = self.output is not None and len(self.output) > 0 self.clear_action.setEnabled(output_exists) self.log_action.setEnabled(output_exists) self.save_action.setEnabled(output_exists) self.kill_if_running() filename = str(self.filecombo.currentText()) if not filename: return self.datatree.load_data(self.DATAPATH) QApplication.processEvents() self.datatree.show_tree() text_style = "%s " date_text = text_style % time.strftime("%d %b %Y %H:%M", time.localtime()) self.datelabel.setText(date_text) def save_data(self): """Save data.""" if not self.output: self.datelabel.setText(_("Nothing to save")) return title = _("Save line profiler result") curr_filename = self.filecombo.currentText() filename, _selfilter = getsavefilename( self, title, f'{curr_filename}_lineprof.txt', _("LineProfiler result") + " (*.txt)", ) if filename: with open(filename, 'w') as f: # for some weird reason, everything is double spaced on Win results = self.output results = results.replace('\r', '') f.write(results) self.datelabel.setText(_(f"Saved results to {filename}")) def update_actions(self): pass class LineProfilerDataTree(QTreeWidget): """ Convenience tree widget (with built-in model) to store and view line profiler data. """ sig_edit_goto_requested = Signal(str, int, str) def __init__(self, parent=None): QTreeWidget.__init__(self, parent) self.header_list = [ _('Line #'), _('Hits'), _('Time (ms)'), _('Per hit (ms)'), _('% Time'), _('Line contents')] self.stats = None # To be filled by self.load_data() self.max_time = 0 # To be filled by self.load_data() self.header().setDefaultAlignment(Qt.AlignCenter) self.setColumnCount(len(self.header_list)) self.setHeaderLabels(self.header_list) self.clear() self.itemClicked.connect(self.on_item_clicked) def show_tree(self): """Populate the tree with line profiler data and display it.""" self.clear() # Clear before re-populating self.setItemsExpandable(True) self.setSortingEnabled(False) self.populate_tree() self.expandAll() for col in range(self.columnCount()-1): self.resizeColumnToContents(col) if self.topLevelItemCount() > 1: self.collapseAll() self.setSortingEnabled(True) self.sortItems(COL_POS, Qt.AscendingOrder) def load_data(self, profdatafile): """Load line profiler data saved by kernprof module""" # lstats has the following layout : # lstats.timings = # {(filename1, line_no1, function_name1): # [(line_no1, hits1, total_time1), # (line_no2, hits2, total_time2)], # (filename2, line_no2, function_name2): # [(line_no1, hits1, total_time1), # (line_no2, hits2, total_time2), # (line_no3, hits3, total_time3)]} # lstats.unit = time_factor with open(profdatafile, 'rb') as fid: lstats = pickle.load(fid) # First pass to group by filename self.stats = dict() linecache.checkcache() for func_info, stats in lstats.timings.items(): # func_info is a tuple containing (filename, line, function anme) filename, start_line_no = func_info[:2] # Read code start_line_no -= 1 # include the @profile decorator all_lines = linecache.getlines(filename) block_lines = inspect.getblock(all_lines[start_line_no:]) # Loop on each line of code func_stats = [] func_total_time = 0.0 next_stat_line = 0 for line_no, code_line in enumerate(block_lines): line_no += start_line_no + 1 # Lines start at 1 code_line = code_line.rstrip('\n') if (next_stat_line >= len(stats) or line_no != stats[next_stat_line][0]): # Line didn't run hits, line_total_time, time_per_hit = None, None, None else: # Compute line times hits, line_total_time = stats[next_stat_line][1:] line_total_time *= lstats.unit time_per_hit = line_total_time / hits func_total_time += line_total_time next_stat_line += 1 func_stats.append( [line_no, code_line, line_total_time, time_per_hit, hits]) # Compute percent time for line in func_stats: line_total_time = line[2] if line_total_time is None: line.append(None) else: line.append(line_total_time / func_total_time) # Fill dict self.stats[func_info] = [func_stats, func_total_time] def fill_item(self, item, filename, line_no, code, time, percent, perhit, hits): item.setData(COL_POS, Qt.UserRole, (osp.normpath(filename), line_no)) item.setData(COL_NO, Qt.DisplayRole, line_no) item.setData(COL_LINE, Qt.DisplayRole, code) if percent is None: percent = '' else: percent = '%.1f' % (100 * percent) item.setData(COL_PERCENT, Qt.DisplayRole, percent) item.setTextAlignment(COL_PERCENT, Qt.AlignCenter) if time is None: time = '' else: time = '%.3f' % (time * 1e3) item.setData(COL_TIME, Qt.DisplayRole, time) item.setTextAlignment(COL_TIME, Qt.AlignCenter) if perhit is None: perhit = '' else: perhit = '%.3f' % (perhit * 1e3) item.setData(COL_PERHIT, Qt.DisplayRole, perhit) item.setTextAlignment(COL_PERHIT, Qt.AlignCenter) if hits is None: hits = '' else: hits = '%d' % hits item.setData(COL_HITS, Qt.DisplayRole, hits) item.setTextAlignment(COL_HITS, Qt.AlignCenter) def populate_tree(self): """Create each item (and associated data) in the tree""" if not self.stats: warn_item = TreeWidgetItem(self) warn_item.setData( 0, Qt.DisplayRole, _('No timings to display. ' 'Did you forget to add @profile decorators ?') .format(url=WEBSITE_URL)) warn_item.setFirstColumnSpanned(True) warn_item.setTextAlignment(0, Qt.AlignCenter) font = warn_item.font(0) font.setStyle(QFont.StyleItalic) warn_item.setFont(0, font) return try: monospace_font = self.window().editor.get_plugin_font() except AttributeError: # If run standalone for testing monospace_font = QFont("Courier New") monospace_font.setPointSize(10) for func_index, stat_item in enumerate(self.stats.items()): # Function name and position func_info, func_data = stat_item filename, start_line_no, func_name = func_info func_stats, func_total_time = func_data func_item = TreeWidgetItem(self) func_item.setData( 0, Qt.DisplayRole, _('{func_name} ({time_ms:.3f}ms) in file "{filename}", ' 'line {line_no}').format( filename=filename, line_no=start_line_no, func_name=func_name, time_ms=func_total_time * 1e3)) func_item.setFirstColumnSpanned(True) func_item.setData(COL_POS, Qt.UserRole, (osp.normpath(filename), start_line_no)) # For sorting by time func_item.setData(COL_TIME, Qt.DisplayRole, func_total_time * 1e3) func_item.setData(COL_PERCENT, Qt.DisplayRole, func_total_time * 1e3) if self.parent().use_colors: color_index = func_index % len(COLOR_CYCLE) else: color_index = 0 func_color = COLOR_CYCLE[color_index] # Lines of code for line_info in func_stats: line_item = TreeWidgetItem(func_item) (line_no, code_line, line_total_time, time_per_hit, hits, percent) = line_info self.fill_item( line_item, filename, line_no, code_line, line_total_time, percent, time_per_hit, hits) # Color background if line_total_time is not None: alpha = percent color = QColor(func_color) color.setAlphaF(alpha) # Returns None color = QBrush(color) for col in range(self.columnCount()): line_item.setBackground(col, color) else: for col in range(self.columnCount()): line_item.setForeground(col, CODE_NOT_RUN_COLOR) # Monospace font for code line_item.setFont(COL_LINE, monospace_font) def on_item_clicked(self, item): data = item.data(COL_POS, Qt.UserRole) if data is None or len(data) < 2: return filename, line_no = data self.sig_edit_goto_requested.emit(filename, line_no, '') # ============================================================================= # Tests # ============================================================================= profile = lambda x: x # dummy profile wrapper to make script load externally @profile def primes(n): """ Simple test function Taken from http://www.huyng.com/posts/python-performance-analysis/ """ if n==2: return [2] elif n<2: return [] s=list(range(3,n+1,2)) mroot = n ** 0.5 half=(n+1)//2-1 i=0 m=3 while m <= mroot: if s[i]: j=(m*m-3)//2 s[j]=0 while j= 900 # time (ms) assert float(top.child(2).data(2, Qt.DisplayRole)) <= 1200 assert float(top.child(3).data(2, Qt.DisplayRole)) <= 100 assert float(top.child(4).data(2, Qt.DisplayRole)) <= 100 assert float(top.child(5).data(2, Qt.DisplayRole)) <= 100