用Spyder开发插件

本研讨会包括对由提供的API的功能和可能性的回顾 Spyder 5,我们最喜欢的科学Python IDE的最新发布版本,用于开发插件以扩展其功能。

作为一个实际练习,我们将开发一个简单的插件,该插件在状态栏中包含一个可配置的Pomodoro计时器和一些工具栏按钮来与其交互。

必备条件

您需要安装Spyder。访问我们的 installation guide 了解更多信息。

重要

Spyder现在提供 独立安装程序 对于Windows和MacOS,无需下载 Python 或在现有环境中手动安装,即可更轻松地启动和运行应用程序。然而,本研讨会的读者应该使用 Python 或Miniconda安装Spyder,因为独立安装程序目前不允许添加额外的软件包,比如我们将在本研讨会中开发的插件。

还需要具备以下先验知识:

  • Python的基础级别。你可以去参观 The Python Tutorial 学习这种编程语言的基础知识。

  • 了解使用Python开发Qt应用程序的基础知识,可以使用 PyQTPySide

为了快速开始使用Qt和Python开发桌面应用程序,这里有一组开放访问资源:

学习目标

在本研讨会结束时,学员将了解:

  • 开发Spyder插件的基础知识,并了解其内部工作原理。

  • 哪些类型的插件可以用Spyder开发。

  • 插件的结构和每个组件的功能,以及它如何连接到Spyder以扩展其功能。

  • 如何打包和发布我们的插件,以便其他人可以轻松安装和使用。

面向开发人员的Spyder

查找有关为Spyder做贡献或为Spyder开发的信息的最佳位置是它的Github存储库,特别是 contribution guide

Spyder IDE components.
  • 的核心 斯派德Spyder-IDE 中开发的桌面应用程序 Qt ,这需要两个与其密切相关的软件包才能运行(没有这两个软件包,它就不能工作): spyder-kernelspython-lsp-server

  • Qt 是一个开源的多平台小部件工具包,用于创建本机图形用户界面。Qt是一个非常完整的开发框架,它提供了构建应用程序的实用工具,并且具有网络、蓝牙、图表、3D渲染、导航(如GPS)等扩展。

  • Spyder使用 qtpy 它是一个抽象层,允许您使用Python中的Qt,而不管您使用的是两个引用库中的任何一个:PyQt或PySide。

  • spyder-kernels 提供Jupyter内核给Spyder,以便在其控制台内使用。

重要

Spyder目前的开发方式是,它的大部分功能都是以插件的形式实现的。

我们可以在Spyder中开发的插件类型

Types of Spyder plugins.

注解

插件是向应用程序添加功能的组件,它可以是图形化组件,例如,用于显示地图,也可以是非图形化组件,用于添加额外的语法配色方案。

形式上,插件是修改Spyder行为的Qt类的实例。除了几个基本组件外,Spyder的大部分功能都来自两种类型插件的交互:

SpyderDockablePlugin

它是一个插件,它的工作方式是 QDockWidget ,这是一个Qt类,它提供可停靠在 QMainWindow 或作为桌面上的顶级窗口浮动。

SpyderPluginV2

SpyderPluginV2 是一个插件,它不会在Spyder的主窗口上创建新的停靠部件。事实上, SpyderPluginV2 是的父类 SpyderDockablePlugin

发现Spyder插件

SpyderDockablePlugin

如果我们查看Spyder界面,我们可以在右侧找到许多不同的窗格(使用默认布局),例如 Help变量资源管理器地块文件历史

  • 这些窗格中的每个窗格都是 SpyderDockablePlugin 这提供了一种 出坞 通过单击右上角的汉堡包菜单按钮进行选择。

  • 这些插件也可以通过它们在 View > Panes 菜单,或使用此处显示的相应键盘快捷键。

SpyderPluginV2

不提供移除选项的高级界面元素基本上是 SpyderPluginV2 。这些通常用于处理更抽象的功能。

  • 这方面的示例包括 外观布局 分别管理Spyder的代码配色方案和窗口布局的插件。

  • 这类插件的其他示例有 主菜单 和键盘 快捷键 。某些图形元素(如主工具栏和状态栏)也是 SpyderPluginV2 班级。

我们该怎么办呢?

我们的实际工作将包括在Spyder接口中实现用于时间管理的Pomodoro技术。

Description of the pomodoro technique.

注解

这个 Pomodoro Technique 由Francesco Cirillo设计,是一种时间管理实践,用于在试图完成任务或满足最后期限时提高您的注意力和工作效率。选择使用波莫多罗计时器可以帮助你全神贯注地完成一项任务。

Pomodoro技术的典型过程包括以下六个步骤:

  1. 选择要完成的任务。

  2. 设置Pomodoro定时器(默认值为25分钟)。

  3. 只处理该任务,直到计时器结束。

  4. 当计时器响起时,在一张纸上打上复选标记,这就是所谓的“柚子”。

  5. 如果复选标记少于3个,请短暂休息(默认为5分钟),然后返回步骤2。

  6. 当你完成了四个Pomodoro循环后,你应该有更长的休息时间(我们的默认休息时间是15分钟)。复选标记重置为零,请返回步骤1。

步骤

以下是我们将在整个研讨会中遵循的一般步骤:

  • 选择最合适的插件类型并使用创建其初始结构 cookiecutter

  • 在我们运行Spyder的虚拟环境中以开发模式安装插件。

  • 使用Spyder类并遵循插件结构中指示的指导原则实现插件的功能。

  • 为我们的插件构建一个配置页面,该页面将出现在Tools>Preferences中。

Location of Spyder Pomodoro Timer widgets in Spyder.

Spyder Pomodoro计时器小工具在Spyder中的位置。

Spyder Pomodoro Timer in the preferences window.

首选项窗口中的Spyder Pomodoro计时器。

功能

组织想法的最小计划。

  • 波莫多罗定时器

    • 状态栏小部件:显示当前Pomodoro间隔的时间。

    • 状态:我们有三个活动状态: 波莫多罗short-breaklong-break 。我们可以显示一条消息(使用 QMessageBox )告诉用户休息的时间到了。

    • 交互:用户可以使用启动、停止和重置按钮来处理Pomodoro计时器。这可以通过添加 QAction 实例。

  • 任务记录器-计数器:我们需要一个变量来计算已完成的Pomodoros的数量。

  • 通知

    • 对话框:每次完成Pomodoro或Break Interval时,都会出现一条消息,提示用户开始处理任务或休息。

    在为任何系统开发插件时,我们必须检查该系统中可用的数据结构和功能,这些数据结构和功能可以促进我们的开发。这需要花费相当多的时间来了解它的内部工作原理。

设置开发环境

原则上,我们可以使用安装在 conda environment 根据 installation guide

但是,如果您使用具有其他依赖项的工作环境,并且希望使插件开发独立于这些依赖项,则建议创建一个仅包含Spyder的新环境,该环境具有您的插件所需的最低依赖项。

Spyder development environment.

我们可以通过以下方式安装它:

$ conda activate base
$ conda install -c conda-forge mamba # A personal recommendation
$ mamba create -n spyder-dev -c conda-forge python=3
$ mamba activate spyder-dev
$ mamba install spyder

注解

  • Anaconda Individual Edition 是一个用于数据科学和机器学习的Python发行版,可在一台机器上使用。

  • Conda 是一个用于管理虚拟环境及其软件包的 Python 工具。

  • Conda可以使用 频道 允许使用不属于官方发行版的软件包。最重要的渠道是 conda-forge ,其中维护了比 Python 个人版提供的包的更广泛和更新的列表。

  • 最后, mamba 是Conda包管理功能的优化实现,它解析依赖项和安装包的速度比Conda快得多。

创建存储库

现在我们有了本地虚拟环境,使用版本控制系统管理我们的源代码是一个很好的实践,目前使用最广泛的Web服务是Github。例如,您可以在这里找到Spyder和Python存储库。

Git and Github repository concepts.

要在Github上创建Git存储库,我们需要执行以下步骤:

  1. 登录到您的Github帐户。

  2. 在您的个人资料图片右上角的“+”菜单中单击“新建存储库”选项。

  3. 将出现一个对话框,您可以在其中插入存储库名称和一些基本选项,例如使用自述文件或许可证文件初始化存储库。

  4. 单击“创建存储库”按钮。

  5. 在最近创建的存储库的主窗口中,单击绿色的“Code”按钮并复制克隆链接。

  6. 在本地命令行运行 $ git clone [repo-link] 。您的计算机上必须安装并配置了Git。如果您没有使用GIT的经验,我们推荐您参加木工作坊 Version control with git

对…的详细描述 repository creation 可以在Github官方文档中找到,并且 hello world Github界面的基本git操作教程。

我们开始吧

我们已经有了一个git存储库和一个安装了Spyder5的虚拟环境。

让我们激活我们的环境并进入存储库的本地文件夹。

mamba activate spyder-dev
cd /path/to/your/repository

那么我们需要使用 cookiecutter 来创建插件的初始结构。 cookiecutter 是用Python制作的工具,专门用于创建项目模板。我们已经开发了其中一个模板来生成插件的基本结构,它可以在以下位置找到:https://github.com/spyder-ide/spyder5-plugin-cookiecutter

Folder structure of our plugin.

让我们运行cookiecuter来生成我们的

$ cookiecutter https://github.com/spyder-ide/spyder5-plugin-cookiecutter
You\'ve downloaded /home/mapologo/.cookiecutters/spyder5-plugin-cookiecutter before. Is it okay to delete and re-download it? [yes]:
full_name [Spyder Bot]: Francisco Palm # It's your name, better John Doe
email [spyder.python@gmail.com]: fpalm@qu4nt.com
github_username [spyder-bot]: map0logo
github_org [spyder-ide]:
project_name [Spyder Boilerplate]: Spyder Pomodoro Timer
project_short_description [Boilerplate needed to create a Spyder Plugin.]: A very simple pomodoro timer that shows in the status bar.
project_pypi_name [spyder-pomodoro-timer]:
project_package_name [spyder_pomodoro_timer]:
pypi_username [map0logo]:
Select plugin_type:
1 - Spyder Dockable Plugin
2 - Spyder Plugin
Choose from 1, 2 [1]: 2
Select open_source_license:
1 - MIT license
2 - BSD license
3 - ISC license
4 - Apache Software License 2.0
5 - GNU General Public License v3
6 - Not open source
Choose from 1, 2, 3, 4, 5, 6 [1]: 1

插件结构

之后 cookicutter 完成其工作后,您将在存储库中获得以下树结构

.
├── [Some info files]
├── Makefile
├── setup.py
├── spyder_pomodoro_timer
│   ├── __init__.py
│   └── spyder
│       ├── __init__.py
│       ├── api.py
│       ├── confpage.py
│       ├── container.py
│       ├── locale
│       │   └── spyder_pomodoro_timer.pot
│       ├── plugin.py
│       └── widgets.py
└── tests

在根文件夹中,您会发现两个重要的文件:

  • Makefile,它有几个有用的命令:

clean                remove all build, test, coverage and Python artifacts
clean-build          remove build artifacts
clean-pyc            remove Python file artifacts
clean-test           remove test and coverage artifacts
test                 run tests quickly with the default Python
docs                 generate Sphinx HTML documentation, including API docs
servedocs            compile the docs watching for changes
release              package and upload a release
dist                 builds source and wheel package
install              install the package to the active Python's site-packages
develop              install the package to the active Python's site-packages
  • setup.py ,帮助您使用安装、打包和分发插件 setuptools ,分发Python模块的标准。在此文件中, entry_points 的参数 setup 是相当重要的,因为它允许Spyder将该软件包标识为插件,并知道如何访问其功能。

这个 spyder-pomodoro-timer 文件夹具有您在运行时引入的名称 cookiecutter 。在该文件夹中,您将看到一个名为 spyder ,我们将在其中放置插件的代码。

spyder 目录中,您将找到以下文件:

  • api.py :插件的功能向世爵睡觉公开。这将允许从其他插件添加附加功能。

  • plugin.py :是插件的核心。根据我们创建的插件类型,这里您将看到 SpyderDockablePluginSpyderPluginV2

    • 如果它是一个 SpyderPluginV2 您应该设置一个名为的常量类 CONTAINER_CLASS 使用一个 PluginMainContainer

    • 如果它是一个 SpyderDockablePlugin 您应该设置一个名为的常量类 WIDGET_CLASS 使用一个 PluginMainWidget

  • container.py :仅用于 SpyderPluginV2 插件。此文件包含 PluginMainContainer 它包含对插件将添加到界面的所有图形元素(或小部件)的引用。这是必要的,因为Qt要求小部件在使用它们之前必须是其他小部件的子级(否则它们会显示为浮动窗口)。因为 SpyderPluginV2 不是窗口小部件,我们需要一个数据结构(即容器)来作为窗口小部件。

  • widgets.py :在此文件中,我们将添加插件的图形组件。如果它的类型是 SpyderPluginV2 而且它没有Widget,那么就没有必要了。我们还可以在这里放置 PluginMainWidget 对于以下项目而言是必要的 SpyderDockablePlugin ,如果我们正在开发这种插件。

  • confpage.py :这是您指定将在中显示的配置页面的位置 Preferences ,以便用户可以调整我们插件的选项。

构建我们的第一个插件

从现在开始,我们将逐步构建插件。在 spyder pomodoro timer repository 您将找到代码的最终版本以供您查看,以防我们遗漏任何细节。

小组件

开始构建我们的插件的最佳方式是首先实现它的图形组件 widgets.py

让我们调用初始版本,不做任何编辑 INITIAL 。在……里面 INITIAL ,widgets.py如下:

# Spyder imports
from spyder.api.config.decorators import on_conf_change
from spyder.api.translations import get_translation

from spyder.api.widgets.mixins import SpyderWidgetMixin


# Localization
_ = get_translation("spyder_pomodoro_timer.spyder")

提示

预设的导入是我们插件中所需内容的指南。这个 on_conf_change 装饰器将允许我们在配置中传播更改。 get_translation 帮助我们为插件生成翻译字符串,并 SpyderWidgetMixin 向任何小部件添加与Spyder集成所需的属性和方法(图标、样式、翻译、操作和额外选项)。

当我们看一看Spyder的时候 api 模块中,我们可以发现,在Spyder中,状态栏有两种类型的预定义组件:

  • StatusBarWidget ,派生自 QWidgetSpyderWidgetMixin ,它包含一个图标、一个标签和一个微调器(用于显示插件加载)。

  • BaseTimerStatus ,派生自 StatusBarWidget 具有内部 QTimer 定期更新其内容。

注解

下面,我们将使用标记之间的差异来指示GitHub中的链接,这有助于检查将在代码中进行的渐进式更改。

我们将在第一版之后到达的第一个版本将被称为 HELLO WORLD

INITIAL -> HELLO WORLD widgets.py diff

由于我们需要一个显示Pomodoro倒计时并定期更新的小部件,因此我们将使用 BaseTimerStatus 实例。

所以,我们可以用替身

from spyder.api.widgets.mixins import SpyderWidgetMixin

使用

from spyder.api.widgets.status import BaseTimerStatus
from spyder.utils.icon_manager import ima

添加初始导入:

# Third party imports
import qtawesome as qta

这样,我们就可以像这样编写我们的第一个小部件了

class PomodoroTimerStatus(BaseTimerStatus):
    """Status bar widget to display the pomodoro timer"""

    ID = "pomodoro_timer_status"
    CONF_SECTION = "spyder_pomodoro_timer"

    def __init__(self, parent):
        super().__init__(parent)
        self.value = "25:00"

    def get_tooltip(self):
        """Override api method."""
        return "I am the Pomodoro timer!"

    def get_icon(self):
        return qta.icon("mdi.av-timer", color=ima.MAIN_FG_COLOR)

提示

Spyder需要 ID 要为其定义 BaseTimerStatus 。它的构造函数调用父类构造函数,并用 value

我们添加一个工具提示来验证我们的小部件是否存在。因为Spyder使用 qtawesome (我们的另一个简化将图标字体合并到PyQt应用程序中的项目),我们可以通过运行 qta-browser 终端上的命令。

(spyder-dev) $ qta-browser

从这里我们可以选择并复制我们喜欢的图标的名称。

qta browser dialog

要完成小部件的实现,我们需要添加以下方法:

# ---- BaseTimerStatus API
def get_value(self):
    """Get current time of the timer"""

    return self.value

BaseTimerStatus 需要实现此方法以在每次内部计时器请求时更新其内容。

集装箱

插件开发的下一步是创建我们上面编写的小部件的一个实例,这样我们就可以将它添加到Spyder的状态栏中。为此,我们需要使用容器。由于Qt的特殊性,我们需要一个 QWidget (容器)作为插件的所有其他部件的父部件(如上所述)。

因此, COOKIECUTTER 版本为 container.py 是:

from spyder.api.config.decorators import on_conf_change
from spyder.api.translations import get_translation
from spyder.api.widgets.main_container import PluginMainContainer

_ = get_translation("spyder_pomodoro_timer.spyder")


class SpyderPomodoroTimerContainer(PluginMainContainer):

    # Signals

    # --- PluginMainContainer API
    # ------------------------------------------------------------------------
    def setup(self):
        pass

    def update_actions(self):
        pass

INITIAL -> HELLO WORLD container.py diff

在这种情况下 SpyderPomodoroTimerContainer 已经定义,并且我们必须实现 setupupdate_actions 方法。

现在我们要将前面创建的小部件添加到容器中。为此,我们首先需要导入小部件。

# Local imports
from spyder_pomodoro_timer.spyder.widgets import PomodoroTimerStatus

然后我们编辑 setup 方法来添加我们的小部件的实例。

def setup(self):
    # Widgets
    self.pomodoro_timer_status = PomodoroTimerStatus(self)

插件

最后,我们定义插件,以便在Spyder中注册它。这个 INITIAL 版本(即由cookiecuter创建的版本) plugin.py 是:

  • 导入:

# Third-party imports
from qtpy.QtGui import QIcon

# Spyder imports
from spyder.api.plugins import Plugins, SpyderPluginV2
from spyder.api.translations import get_translation

# Local imports
from spyder_pomodoro_timer.spyder.confpage import SpyderPomodoroTimerConfigPage
from spyder_pomodoro_timer.spyder.container import SpyderPomodoroTimerContainer

_ = get_translation("spyder_pomodoro_timer.spyder")
  • 插件类:

class SpyderPomodoroTimer(SpyderPluginV2):
    """
    Spyder Pomodoro Timer plugin.
    """

    NAME = "spyder_pomodoro_timer"
    REQUIRES = []
    OPTIONAL = []
    CONTAINER_CLASS = SpyderPomodoroTimerContainer
    CONF_SECTION = NAME
    CONF_WIDGET_CLASS = SpyderPomodoroTimerConfigPage

    # --- Signals

    # --- SpyderPluginV2 API
    # ------------------------------------------------------------------------
    def get_name(self):
        return _("Spyder Pomodoro Timer")

    def get_description(self):
        return _("A very simple pomodoro timer")

    def get_icon(self):
        return QIcon()

    def register(self):
        container = self.get_container()
        print('SpyderPomodoroTimer registered!')

    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

INITIAL -> HELLO WORLD plugin.py diff

首先,我们需要声明插件的依赖项,方法是定义 REQUIRES 类常量。由于我们要添加状态栏小部件,因此我们需要 StatusBar 插件,如下所示。

REQUIRES = [Plugins.StatusBar]

然后我们需要为我们的插件设置图标。为此,我们替身

from qtpy.QtGui import QIcon

# ...

def get_icon(self):
    return QIcon()

通过

# Third-party imports
import qtawesome as qta

# Spyder imports
from spyder.utils.icon_manager import ima

def get_icon(self):
    return qta.icon("mdi.av-timer", color=ima.MAIN_FG_COLOR)

由于最近对Spyder API的更改,我们需要添加到Spyder导入

# Spyder imports
from spyder.api.plugin_registration.decorators import on_plugin_available

和改变,

def register(self):
    container = self.get_container()
    print('SpyderPomodoroTimer registered!')

def on_initialize(self):
    print("SpyderPomodoroTimer registered!")

@on_plugin_available(plugin=Plugins.StatusBar)
def on_statusbar_available(self):
    statusbar = self.get_plugin(Plugins.StatusBar)
    if statusbar:
        statusbar.add_status_widget(self.pomodoro_timer_status)

有了这些更改,Spyder将意识到我们的插件的存在,并且这个插件会向状态栏添加一个新的小部件。

最后,我们将以下方法添加到我们的插件中:

@property
def pomodoro_timer_status(self):
    container = self.get_container()
    return container.pomodoro_timer_status

这样一来, SpyderPomodoroTimer 可以访问 pomodoro_timer_statusSpyderPomodoroTimerContainer 就像它是自己的财产一样。

综上所述,我们做了以下工作:

Basic structure of Pomodoro Timer Spyder plugin.

我们创建了一个小部件,然后将其添加到容器中,该容器通过 CONTAINER_CLASS 常量。在插件中,我们访问了该小部件的实例并将其添加到状态栏。

如何测试我们的插件

现在是时候看看我们的插件在Spyder界面中是什么样子了。

从插件的根文件夹中,我们激活安装Spyder的环境,并运行:

(base) $ mamba activate spyder-dev
(spyder-dev) $ pip install -e .

现在我们可以看到两个输出。第一个显示在命令行中:

(spyder-dev) $ spyder
SpyderPomodoroTimer registered!

在Spyder中,你会在状态栏看到我们的插件,工具提示是“我是Pomodoro工具提示”。

First version of our plugin

请记住,每次我们对代码进行更改时,都必须重新启动Spyder,以便重新加载插件并检查更改。

增强我们的插件

从现在开始,我们将详细介绍在Qt中是如何实现的。因此,如果您有任何疑问,Qt文档将是您最好的指南。我们为本研讨会创建了一个附件,为赶时间的人快速解释Qt的基本概念: Qt基础知识

计时器更新

我们插件的第一个问题是它的Pomodoro计时器没有更新。要激活它,我们可以使用 QTimer 在……里面 PomodoroTimerStatus ,它之所以存在,是因为它是 BaseTimerStatus

更新状态栏中的值的第二个版本称为 TIMER

让我们回过头来看看 widgets.py 并在导入行(第22行)下面添加此常量。

HELLO WORLD -> TIMER widgets.py diff

# --- Constants
# ------ Time limits by default

POMODORO_DEFAULT = 25 * 60 * 1000  # 25 mins in milliseconds
INTERVAL = 1000

POMODORO_DEFAULT 是以毫秒为单位设置Pomodoro时间限制,以及 INTERVAL 设置为定时器更新率。

现在,在 __init__ 一种方法 PomodoroTimerStatus 我们需要添加:

# Actual time limits
self.pomodoro_limit = POMODORO_DEFAULT
self.countdown = self.pomodoro_limit

self._interval = INTERVAL
self.timer.timeout.connect(self.update_timer)
self.timer.start(self._interval)

到目前为止,我们创建了一个默认值 (POMODORO_DEFAULT )的计时器持续时间;我们将其添加到 pomodoro_limit 属性以便能够对其进行配置;并使用该值初始化 countdown 属性,该属性将随着时间的推移而修改。至于计时器的更新间隔,我们将其设置为 INTERVAL ,相当于1秒(1000毫秒)。

的功能 self.timer 就是定期更新我们的计时器。这是通过方法 timeout.connect() ,我们将对它的引用作为参数传递给 update_timer 将执行所需调整的函数。

现在让我们实现 update_timer 在文件末尾:

def display_time(self):
    """Calculate the time that should be displayed."""

    minutes = int((self.countdown / (1000 * 60)) % 60)
    seconds = int((self.countdown / 1000) % 60)
    return f"{minutes:02d}:{seconds:02d}"

def update_timer(self):
    """Updates the timer and the current widget. Also, update the
    task counter if a task is set."""

    if self.countdown > 0:
        # Update the current timer by decreasing the current running time by one second
        self.countdown -= INTERVAL
        self.value = self.display_time()

在这里,我们依赖于 display_time 方法,该方法将当前 countdown 值(以毫秒为单位)转换为人类可读的格式。和 update_timer 简单地不断更新倒计时,直到它达到零。

如果我们再次运行Spyder,我们会发现我们的计时器已经启动。

Timer countdown working.

计时器控件

现在我们需要一种方法来控制我们的计时器。我们可以通过向Spyder的工具栏添加一些按钮来实现这一点,这将有助于学习如何在Spyder中使用工具栏、菜单和操作。

PomodoroTimerToolbar

将操作添加到工具栏的下一个版本称为 ACTIONS

TIMER -> ACTIONS widgets.py diff

让我们回过头来看看 widgets.py 并导入Spyder应用程序工具栏类:

from spyder.api.widgets.toolbars import ApplicationToolbar

并通过在定义之前添加以下代码来创建它的实例 PomodoroTimerStatus

class PomodoroTimerToolbar(ApplicationToolbar):
    """Toolbar to add buttons to control our timer."""

    ID = 'pomodoro_timer_toolbar'

如您所见,此语句非常简单。它只需要声明一个 ID ,这有助于在睡觉中识别我们的工具。

也可以在我们的工具栏中包含其他Qt小部件,但在这种情况下,最好使用适当的Spyder方法,以维护它们与应用程序的睡觉的关系。换句话说,只要您需要的小部件存在于 spyder.api.widgets ,使用它!

接下来,我们需要在Status小部件中声明一个布尔变量,以指示倒计时是否暂停。为此,让我们将以下内容添加到 __init__ 一种方法 PomodoroTimerStatus

self.pause = True

并且在内部的 update_timer 方法,替身

if self.countdown > 0:
    ...

通过

if self.countdown > 0 and not self.pause:
    ...

创建Pomodoro工具栏

现在,我们将在工具栏中创建一个新部分,并通过操作将一些功能与其相关联。建议将此特定信息包含在 api.py 文件,因为通过这种方式,我们可以提供Spyder睡觉的端点和用于调整插件行为的新插件。

TIMER -> ACTIONS api.py diff

让我们将以下内容添加到 api.py

class PomodoroToolbarActions:
    Start = 'start_timer'
    Pause = 'pause_timer'
    Stop = 'stop_timer'


class PomodoroToolbarSections:
    Controls = "pomodoro_timer"

class PomodoroMenuSections:
    Main = "main_section"

有了这些,我们告诉世爵的睡觉和我们自己的插件,我们将有一个新的工具栏部分,叫做“pomodoro_Timer”。此部分将由一个按钮组成,该按钮包含一个菜单(具有单个部分“main_Section”)和标识为“Start_Timer”、“Pause_Timer”和“Stop_Timer”的操作,以分别启动、暂停和停止(重置)我们的计时器。

请注意,这些是带有类常量的简单类定义,以便以简单的方式简化此信息的封装和交换。

向工具栏添加操作

TIMER -> ACTIONS container.py diff

现在让我们来看看 container.py ,我们将在其中实现新工具栏的行为及其操作。在这种情况下,我们不会指定插件的内部行为,而是指定它的小部件和Spyder的其他区域之间的关系,所以在容器中进行会更方便。

就像我们以前做的那样 PomodoroTimerStatus ,我们将使用 qtawesome 我们行动的图标。为此,让我们在导入的开头添加:

# Third party imports
import qtawesome as qta
from qtpy.QtWidgets import QToolButton

我们还进口了 QToolButton 因为它将用于设置我们将在工具栏中添加的按钮。

在Spyder进口结束时,我们还需要:

from spyder.utils.icon_manager import ima

现在,让我们包括 PomodoroTimerToolbar 中声明的操作和部分 api.py 在我们的本地进口产品中:

from spyder_pomodoro_timer.spyder.widgets import (
    PomodoroTimerStatus,
    PomodoroTimerToolbar,
)
from spyder_pomodoro_timer.spyder.api import (
    PomodoroToolbarActions,
    PomodoroToolbarSections,
    PomodoroMenuSections,
)

接下来,我们需要在 setup 一种方法 SpyderPomodoroTimerContainer

第一个步骤是创建我们在前面声明的Toolbar类的一个实例:

title = _("Pomodoro Timer Toolbar")
self.pomodoro_timer_toolbar = PomodoroTimerToolbar(self, title)

第二个是创建与启动、暂停和停止Pomodoro计时器相对应的操作:

# Actions
start_timer_action = self.create_action(
    PomodoroToolbarActions.Start,
    text=_("Start"),
    tip=_("Start timer"),
    icon=qta.icon("fa.play-circle", color=ima.MAIN_FG_COLOR),
    triggered=self.start_pomodoro_timer,
)

pause_timer_action = self.create_action(
    PomodoroToolbarActions.Pause,
    text=_("Pause"),
    tip=_("Pause timer"),
    icon=qta.icon("fa.pause-circle", color=ima.MAIN_FG_COLOR),
    triggered=self.pause_pomodoro_timer,
)

stop_timer_action = self.create_action(
    PomodoroToolbarActions.Stop,
    text=_("Stop"),
    tip=_("Stop timer"),
    icon=qta.icon("fa.stop-circle", color=ima.MAIN_FG_COLOR),
    triggered=self.stop_pomodoro_timer,
)

第三个步骤是创建包含我们的操作的菜单,并将它们添加到菜单中。

self.pomodoro_menu = self.create_menu(
    "pomodoro_timer_menu",
    text=_("Pomodoro timer"),
    icon=qta.icon("mdi.av-timer", color=ima.MAIN_FG_COLOR),
)

# Add actions to the menu
for action in [start_timer_action, pause_timer_action, stop_timer_action]:
    self.add_item_to_menu(
        action,
        self.pomodoro_menu,
        section=PomodoroMenuSections.Main,
    )

第四个步骤是创建一个将包含菜单的按钮,并将其配置为 PopupMode ,以便在单击时显示。

self.pomodoro_button = self.create_toolbutton(
    "pomodoro_timer_button",
    text=_("Pomodoro timer"),
    icon=qta.icon("mdi.av-timer", color=ima.MAIN_FG_COLOR),
)

self.pomodoro_button.setMenu(self.pomodoro_menu)
self.pomodoro_button.setPopupMode(QToolButton.InstantPopup)

最后,第五个是将按钮添加到我们的工具栏中:

# Add menu to toolbar
self.add_item_to_toolbar(
    self.pomodoro_button,
    self.pomodoro_timer_toolbar,
    section=PomodoroToolbarSections.Controls,
)

在创建操作时,我们通过 triggered 参数激活方法时(即单击工具栏上的相应按钮时)要执行的方法。

我们可以将这些方法插入到 SpyderPomodoroTimerContainer 声明,在Cookiecuter模板指示为 # --- Public API

def start_pomodoro_timer(self):
    """Start the timer."""
    self.pomodoro_timer_status.timer.start(1000)
    self.pomodoro_timer_status.pause = False

def pause_pomodoro_timer(self):
    """Pause the timer."""
    self.pomodoro_timer_status.timer.stop()
    self.pomodoro_timer_status.pause = True

def stop_pomodoro_timer(self):
    """Stop the timer."""
    self.pomodoro_timer_status.timer.stop()
    self.pomodoro_timer_status.pause = True
    self.pomodoro_timer_status.countdown = self.pomodoro_timer_status.pomodoro_limit

这些方法只是简单地操作 pause 的字段 pomodoro_timer_status ,并且在以下情况下 stop_pomodoro_timer 重新开始倒计时。

注册工具栏

TIMER -> ACTIONS plugin.py diff

最后一个强制性步骤是转到 plugin.py 并注册这个新的工具栏组件。

要执行此操作,请添加 Plugins.Toolbar 符合插件要求:

REQUIRES = [Plugins.StatusBar, Plugins.Toolbar]

并使用此插件的API将我们在容器中创建的工具栏添加到Spyder的工具栏中。

@on_plugin_available(plugin=Plugins.Toolbar)
def on_toolbar_available(self):
    container = self.get_container()
    toolbar = self.get_plugin(Plugins.Toolbar)
    toolbar.add_application_toolbar(container.pomodoro_timer_toolbar)

查看更改

我们可以注意到的第一件事是,我们在工具栏中已经有了相应的按钮。

Pomodoro timer toolbar buttons

输入的字符串作为 tip 参数在此处显示为按钮的工具提示。

此外,如果我们检查菜单“View>Toolbar”,我们会发现那里有一个与我们的工具栏相对应的新条目。

View > Toolbars menu with "Pomodoro Timer Toolbar" option.

最后,让我们检查一下工具栏中新的Pomodoro Timer控件按钮如何与状态栏中的组件交互。

Interaction between the Pomodoro Timer toolbar and its status bar.

添加配置页

Spyder插件的另一个特点是它们可以有可配置的选项出现在Spyder的首选项窗口中。

配置默认值

我们在其中添加可配置参数的最终版本将被称为 CONFPAGE

第一步是定义我们想要向用户提供哪些选项。为此,我们必须创建一个新文件,我们可以将其称为 conf.py 。在此文件中,我们将编写以下内容:

ACTIONS -> CONFPAGE config.py diff

"""Spyder terminal default configuration."""

# --- Constants
# ------ Time limits by default

POMODORO_DEFAULT = 25 * 60 * 1000  # 25 mins in milliseconds

CONF_SECTION = "spyder_pomodoro_timer"

CONF_DEFAULTS = [
    (
        CONF_SECTION,
        {
            "pomodoro_limit": POMODORO_DEFAULT / (60 * 1000),
        },
    ),
    ("shortcuts", {"pomodoro-timer start/pause": "Ctrl+Alt+Shift+P"}),
]

我们必须突出宣布 CONF_SECTION ,这是与我们的插件相对应的首选项中的节的内部名称;以及与 CONF_DEFAULTS 。在这种情况下,我们表示 pomodoro_limit 中的可配置参数。 spyder_pomodoro_timer 部分。

在该文件的末尾需要设置另一个重要常数, CONF_VERSION ,当在插件的连续版本中添加、删除或重命名可配置参数时,必须更新。

# 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 = "1.0.0"

请注意,我们正在移动 POMODORO_DEFAULT 从… widgets.pyconf.py ,因为我们现在有一个专门的位置来存放默认配置值。

配置页面

现在,我们需要构建将出现在Preferences窗口中的页面。为此,我们编辑 confpage.py cokkiecuter生成的文件如下:

ACTIONS -> CONFPAGE confpage.py diff

"""
Spyder Pomodoro Timer Preferences Page.
"""
from qtpy.QtWidgets import QGridLayout, QGroupBox, QVBoxLayout
from spyder.api.preferences import PluginConfigPage
from spyder.api.translations import get_translation

from spyder_pomodoro_timer.spyder.config import POMODORO_DEFAULT

_ = get_translation("spyder_pomodoro_timer.spyder")


class SpyderPomodoroTimerConfigPage(PluginConfigPage):

    # --- PluginConfigPage API
    # ------------------------------------------------------------------------
    def setup_page(self):
        limits_group = QGroupBox(_("Time limits"))
        pomodoro_spin = self.create_spinbox(
            _("Pomodoro timer limit"),
            _("min"),
            "pomodoro_limit",
            default=POMODORO_DEFAULT,
            min_=5,
            max_=100,
            step=1,
        )

        pt_limits_layout = QGridLayout()
        pt_limits_layout.addWidget(pomodoro_spin.plabel, 0, 0)
        pt_limits_layout.addWidget(pomodoro_spin.spinbox, 0, 1)
        pt_limits_layout.addWidget(pomodoro_spin.slabel, 0, 2)
        pt_limits_layout.setColumnStretch(1, 100)
        limits_group.setLayout(pt_limits_layout)

        vlayout = QVBoxLayout()
        vlayout.addWidget(limits_group)
        vlayout.addStretch(1)
        self.setLayout(vlayout)

这主要对应于基于Qt小部件的用户界面的常规代码。在本例中,我们的选项部分对应于 QGroupBox ,其中参数是使用 QVBoxLayout ,并且每个参数对应于一个 QGridLayout 标签和输入分布的位置(在本例中为 QSpinBox )。

Spyder中的配置页提供了一些帮助程序方法来简化此工作。例如, create_spinbox 允许在单个步骤中实例化和初始化Widget对应的前缀、后缀、标签和旋转框。

传播配置更改

由于我们将所有配置信息移动到 conf.py ,现在我们必须将它从那里导入到 widgets.py

ACTIONS -> CONFPAGE widgets.py diff

# Local imports
from spyder_pomodoro_timer.spyder.config import (
    CONF_SECTION,
    CONF_DEFAULTS,
    CONF_VERSION,
)

现在,我们可以从插件中的任何位置使用 get_conf 方法。在本例中,我们使用它来访问 pomodoro_limit 来自配置,而不是常量 POMODORO_DEFAULT

self.pomodoro_limit = self.get_conf(
    "pomodoro_limit"
)

现在我们可以添加一个更新可配置参数的方法 pomodoro_limit 。这个 @on_conf_change 装饰者负责捕获应用特定选项更改时生成的信号。

@on_conf_change(option="pomodoro_limit")
def set_pomodoro_limit(self, value):
    self.pomodoro_limit = int(value) * 1000 * 60
    self.countdown = self.pomodoro_limit
    self.value = self.display_time()

注册首选项

最后,有必要在中激活首选项的使用 plugin.py ,通过需要首选项插件

ACTIONS -> CONFPAGE plugin.py diff

class SpyderPomodoroTimer(SpyderPluginV2):
    ...
    REQUIRES = [Plugins.Preferences, Plugins.StatusBar, Plugins.Toolbar]

并将我们的插件注册到装饰器的方法中 @on_plugin_available

@on_plugin_available(plugin=Plugins.Preferences)
def on_preferences_available(self):
    preferences = self.get_plugin(Plugins.Preferences)
    preferences.register_plugin_preferences(self)

现在,我们可以从工具栏或从“Tools”>Preferences“菜单访问Preferences窗口。在那里我们会找到一个叫 Spyder Pomodoro定时器 在它的内部是 Pomodoro定时器限制 参数。如果我们更改该值,我们将看到状态栏中相应的标签是如何更改的。

Pomodoro Timer toolbar configuration page.

现在您的插件已经是初始版本,可以发布了……

发布您的插件

既然推荐的安装Spyder的方式是通过Conda,那么显而易见的选择就是通过像Conda-Forge这样的渠道发布我们的插件,但是由于它的复杂性,这项任务超出了本研讨会的范围。

然而,用于在Conda中发布包的工具通常基于在PyPI中发布的包。那么让我们看看如何在那里发布我们的插件。

Publish your plugin in PyPI.

PyPI和TestPyPI

我们要做的第一件事是在 PyPITestPyPI 网站。虽然我们的包最终将以PyPI发布,但建议使用TestPyPI测试我们的包是否可以正确发布,而不会对PyPI服务器产生额外的负载或影响它们的日志。

接下来,我们需要编辑 setup.py 在我们的项目根目录下使用我们自己的数据创建文件。幸运的是,Cookiecuter为我们创建了一个。

要将我们的包上传到PyPI,我们必须使用一个名为 Twine 这使得这项任务变得容易得多。我们可以使用以下命令将其安装在conda环境中:

$ mamba install twine

生成并检查包

在发布插件之前,我们必须将其打包。要做到这一点,我们必须从项目的根文件夹(其中 setup.py 已放置):

$ python setup.py sdist bdist_wheel

之后,我们将看到以下文件在 dist 文件夹:

spyder_pomodoro_timer
└── dist
    ├── spyder_pomodoro_timer-0.0.1.dev0-py3-none-any.whl
    └── spyder-pomodoro-timer-0.0.1.dev0.tar.gz

在Linux和MacOS上,我们可以通过检查 tar 文件:

$ tar tzf dist/spyder-pomodoro-timer-0.0.1.dev0.tar.gz

您还可以使用 twine 要对中创建的文件运行检查,请执行以下操作 dist

$ twine check dist/*
Checking dist/spyder_pomodoro_timer-0.0.1.dev0-py3-none-any.whl: PASSED
Checking dist/spyder-pomodoro-timer-0.0.1.dev0.tar.gz: PASSED

上传到PyPI

现在我们可以使用TWINE上传我们构建的分发包。首先,我们会将它们上传到TestPyPI,以确保一切正常:

$ twine upload --repository-url https://test.pypi.org/legacy/ dist/*

此命令将提示您输入您在TestPyPI中注册时使用的用户名和密码。

如果我们在浏览器中打开https://test.pypi.org/project/spyder-pomodoro-timer/,我们将能够看到我们刚刚发布的包。

在那里,我们将看到缺少一些详细信息,如包描述,并且我们的包被标记为 Development Status 5-Stable

要修复第一个问题,我们可以按照中的说明进行操作 Making a PyPI-friendly README 。因为我们已经有了一个自述文件,所以我们只需将以下几行添加到 setup.py 文件:

# read the contents of your README file
from pathlib import Path
this_directory = Path(__file__).parent
long_description = (this_directory / "README.md").read_text()

setup(
    name="spyder-pomodoro-timer",
    # ...
    long_description=long_description,
    long_description_content_type='text/markdown'
)

我们还可以使用以下站点作为指南更改软件包的分类器:https://pypi.org/classifiers.在这里,我们可以简单地复制我们认为合适的分类器,然后将它们粘贴到我们的代码中。特别是在 setup.py ,在作为 classifier 函数调用中的参数 setup

通过这些更改,并通过在 __init__.py 文件中的 spyder_pomodoro_timer 文件夹中,我们可以重复构建新版本的包,将其加载到TestPyPI进行检查,最后使用以下命令将其加载到PyPI的循环:

$ twine upload dist/

并在https://pypi.org/project/spyder-pomodoro-timer/中检查结果

完成此操作后,任何人只需运行以下命令即可在其环境中安装我们的插件:

$ pip install spyder-pomodoro-timer

最后一句话

通过插件、扩展或加载项(通常称为插件、扩展或插件)使工具可扩展的可能性是一个基本功能,它允许利用第三方开发人员的才华来响应超出应用程序核心开发团队范围的需求和增强功能。

同样,基于插件的系统使应用程序更易于维护。最终,启用和禁用插件的能力使其更能适应不同的用例。例如,目前很难想象会有一个没有挡路广告扩展或组织链接的网络浏览器,即使这些功能不是默认的。

在Spyder中,我们对整合API特别感兴趣,该API允许以一致的方式开发插件。版本4和版本5之间的开发工作的主要焦点就是这个方向,我们正处于一个关键时刻,我们希望利用所有这些工作。

在本研讨会中,您学习了如何执行以下操作:

  • 确定Spyder开发中的基本构件。

  • 识别可以在Spyder中实现的不同类型的插件。

  • 识别Spyder的插件类型。

  • 计划开发一个新的Spyder插件。

  • 为Spyder插件开发构建开发环境。

  • 使用Cookiecuter生成Spyder插件的基本结构。

  • 了解Spyder插件的文件结构。

  • 在Spyder状态栏中添加和注册Qt小部件。

  • 在Spyder工具栏中添加和注册Qt小部件。

  • 在工具栏中添加带有操作的菜单。

  • 将配置选项添加到我们的插件中,并在Preferences窗口中显示它们。

  • 编辑我们插件的可安装软件包的描述和分类器。

  • 将我们的插件发布到TestPyPI和PyPI。

有了这些技能,我们希望为您开发自己的Spyder插件提供便利。

如果您对插件开发有想法,请随时通过 Spyder-IDE GitHub组织空间。

如果您对Spyder的科学计算入门感兴趣,可以访问研讨会 Scientific Computing and Visualization with Spyder

如果您对Spyder财务分析入门感兴趣,可以访问研讨会 Financial Data Analysis with Spyder

家庭作业

正如您可能已经注意到的,还有一些功能需要实现,比如当pomodoros完成时会发出通知。尝试执行它们,如果您有任何疑问,请不要犹豫与我们联系。

进一步阅读

plugin-examples 资源库,您可以找到更多的示例,这些示例肯定会对您进一步理解Spyder插件开发非常有用。

更深入地回顾Spyder存储库本身,特别是其更简单的插件,如历史记录、绘图或工作目录,可能会帮助您更好地理解它。中提供的各种帮助器函数、小部件和混合 spyder.api