使用 Spyder 进行插件开发#
本次研讨会将回顾 Spyder 5(我们最喜爱的科学 Python IDE 的最新发布版本)为插件开发和扩展其功能所提供的 API 的特性和可能性。
作为一项实践练习,我们将开发一个简单的插件,它在状态栏中集成一个可配置的番茄工作法计时器,并提供一些工具栏按钮与之交互。
先决条件#
您需要安装 Spyder。请访问我们的安装指南以获取更多信息。
重要
Spyder 现在为 Windows 和 macOS 提供了独立安装程序,这使得启动和运行应用程序变得更加容易,无需下载 Anaconda 或在现有环境中手动安装。然而,本次研讨会的读者应该使用 Anaconda 或 Miniconda 安装 Spyder,因为独立安装程序目前不允许添加额外的包,例如我们将在本次研讨会中开发的插件。
此外,最好具备以下先验知识
为了快速开始使用 Qt 和 Python 进行桌面应用程序开发,这里提供了一系列开放访问资源:
学习目标#
在本研讨会结束时,参与者将了解:
为 Spyder 开发插件的基础知识,并对其内部工作原理有一个大致了解。
可以使用 Spyder 开发的插件类型。
插件的结构以及每个组件的功能,以及它如何连接到 Spyder 以扩展其功能。
如何打包和发布我们的插件,以便其他人可以轻松安装和使用。
面向开发者的 Spyder#
查找有关贡献或为 Spyder 开发的信息的最佳地点是其 Github 仓库,特别是贡献指南。

Spyder 的核心是 Spyder-IDE,这是一个用 Qt 开发的桌面应用程序,其运行需要两个与其密切相关的包(没有它们就无法工作):spyder-kernels 和 python-lsp-server。
Qt 是一个开源多平台小部件工具包,用于创建原生图形用户界面。Qt 是一个非常完整的开发框架,提供用于构建应用程序的实用程序,并具有网络、蓝牙、图表、3D 渲染、导航(如 GPS)等扩展。
Spyder 使用 qtpy,这是一个抽象层,允许您从 Python 处理 Qt,无论您使用 PyQt 还是 PySide 这两个参考库中的哪一个。
spyder-kernels 为 Spyder 提供 Jupyter 内核,供其控制台内部使用。
重要
Spyder 目前的开发方式是其大部分功能都以插件的形式实现。
我们可以在 Spyder 中开发的插件类型#

注意
插件是为应用程序添加功能的组件,它可以是图形组件(例如显示地图),也可以是非图形组件(例如添加额外的语法着色方案)。
正式地,插件是 Qt 类的实例,它们修改 Spyder 的行为。除了一些基本组件外,Spyder 的大部分功能都来自两种类型插件的交互:
SpyderDockablePlugin#
它是一个作为 QDockWidget 工作的插件,这是一个 Qt 类,提供了一个图形控件,可以停靠在 QMainWindow 内部,或作为顶级窗口浮动在桌面上。
SpyderPluginV2#
SpyderPluginV2
是一个不会在 Spyder 主窗口上创建新停靠小部件的插件。事实上,SpyderPluginV2
是 SpyderDockablePlugin
的父类。
发现 Spyder 插件#
我们要做什么?#
我们的实际工作将是在 Spyder 界面中实现番茄工作法的时间管理技术。

注意
番茄工作法由 Francesco Cirillo 设计,是一种时间管理实践,用于在尝试完成任务或达到截止日期时提高您的注意力和生产力。选择使用番茄工作法计时器可以帮助您全身心地投入到一项任务中。
番茄工作法的典型过程包括以下六个步骤:
选择一项要完成的任务。
设置番茄工作法计时器(默认为 25 分钟)。
只专注于该任务,直到计时器结束。
计时器响铃时,在纸上打一个勾,这称为“一个番茄”。
如果您的勾号少于 3 个,则进行短暂休息(默认为 5 分钟),然后返回步骤 2。
当您完成四个番茄周期后,您将获得更长时间的休息(我们的默认是 15 分钟)。勾号重置为零,返回步骤 1。
步骤#
以下是我们将在本次研讨会中遵循的总体步骤:
选择最合适的插件类型,并使用 cookiecutter 创建其初始结构。
在运行 Spyder 的虚拟环境中以开发模式安装插件。
使用 Spyder 类并遵循插件结构中指示的准则实现插件的功能。
为我们的插件构建一个配置页面,该页面将出现在“工具 > 首选项”中。

Spyder 番茄工作法计时器小部件在 Spyder 中的位置。#

首选项窗口中的 Spyder 番茄工作法计时器。#
功能#
一个最小的规划来组织想法。
番茄工作法计时器
状态栏小部件:显示当前番茄间隔的时间。
状态:我们有三种活动状态:番茄工作、短休息和长休息。我们可以显示一条消息(使用 QMessageBox)来告诉用户休息时间到了。
交互:用户可以使用“开始”、“停止”和“重置”按钮来操作番茄工作法计时器。这可以通过在工具栏的菜单中添加 QAction 实例来实现。
任务日志 - 计数器:我们需要一个变量来计算已完成的番茄数量。
通知
对话框:每次完成一个番茄或休息间隔时,应出现一条消息,提示用户开始工作或休息。
在为任何系统开发插件时,我们必须检查该系统中可用的数据结构和函数,这些可以促进我们的开发。这需要花费大量时间来理解其内部工作原理。
设置开发环境#
原则上,我们可以使用根据安装指南在conda 环境中安装的任何 Spyder。
但是,如果您使用的工作环境有其他依赖项,并且希望您的插件开发独立于它们,建议创建一个只包含 Spyder 及其插件所需最少依赖项的新环境。

我们可以按以下方式安装它:
$ 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 是一个 Anaconda 工具,用于管理虚拟环境及其包。
Conda 可以与通道一起使用,这允许使用不属于官方发行版的包。最重要的通道是conda-forge,它维护着比 Anaconda Individual Edition 提供的更广泛和更新的包列表。
最后,mamba 是 conda 包管理功能的优化实现,它比 conda 更快地解决依赖关系和安装包。
创建仓库#
现在我们已经有了本地虚拟环境,用版本控制系统管理我们的源代码是很好的做法,目前最广泛使用的网络服务是 Github。例如,您可以在这里找到 Spyder 和 Python 的仓库。

要在 Github 上创建 Git 仓库,我们需要遵循以下步骤:
登录您的 Github 账户。
点击您个人资料图片旁边右上角“+”菜单中的“New repository”选项。
将出现一个对话框,您可以在其中插入仓库名称和一些基本选项,例如使用 README 或许可文件初始化仓库。
点击“Create repository”按钮。
在新创建的仓库主窗口中,点击绿色的“Code”按钮并复制克隆链接。
在您的本地命令行运行
$ git clone [repo-link]
。您必须在计算机上安装并配置 Git。如果您没有使用 Git 的经验,我们推荐 The Carpentries 的研讨会 Git 版本控制。
关于仓库创建的详细说明可以在 Github 官方文档中找到,以及一个带有 Github 界面基本 Git 操作的“Hello world”教程。
开始吧#
我们已经有了一个 Git 仓库,以及一个安装了 Spyder 5 的虚拟环境。
让我们激活我们的环境并进入我们仓库的本地文件夹。
mamba activate spyder-dev
cd /path/to/your/repository
然后我们需要使用 cookiecutter
来创建我们插件的初始结构。cookiecutter 是一个用 Python 制作的工具,专门用于创建项目模板。我们已经开发了其中一个模板来生成插件的基本结构,它可以在这里找到:spyder-ide/spyder5-plugin-cookiecutter

让我们运行 cookiecutter 来生成我们的插件。
$ 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 [[email protected]]: [email protected]
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 模块分发的标准)安装、打包和分发您的插件。在此文件中,setup
的entry_points
参数非常重要,因为它允许 Spyder 将此包识别为插件,并知道如何访问其功能。
spyder-pomodoro-timer
文件夹的名称是您运行 cookiecutter
时输入的名称。在此文件夹内,您会看到一个名为 spyder
的文件夹,我们将在其中放置插件的代码。
在 spyder
目录中,您会找到以下文件:
api.py
:插件的功能在这里暴露给 Spyder 的其余部分。这将允许从其他插件添加额外的功能。plugin.py
:是插件的核心。根据我们创建的插件类型,您会在这里看到SpyderDockablePlugin
或SpyderPluginV2
的实例。如果它是
SpyderPluginV2
,您应该设置一个名为CONTAINER_CLASS
的常量类,其中包含PluginMainContainer
的实例。如果它是
SpyderDockablePlugin
,您应该设置一个名为WIDGET_CLASS
的常量类,其中包含PluginMainWidget
的实例。
container.py
:仅用于SpyderPluginV2
插件。此文件包含一个PluginMainContainer
实例,该实例包含对插件将添加到界面的所有图形元素(或小部件)的引用。这是必要的,因为 Qt 要求小部件在使用前必须是其他小部件的子级(否则它们会显示为浮动窗口)。由于SpyderPluginV2
不是小部件,因此我们需要一个作为小部件的数据结构(即容器)。widgets.py
:在这个文件中,我们将添加我们插件的图形组件。如果它是SpyderPluginV2
类型且没有小部件,则不需要。我们也可以将PluginMainWidget
的实例放在这里,如果它对于SpyderDockablePlugin
是必需的,如果我们在开发那种插件的话。confpage.py
:这是您指定将显示在Preferences
中的配置页面的地方,以便用户可以调整我们插件的选项。
构建我们的第一个插件#
从现在开始,我们将逐步构建插件。在spyder 番茄工作法计时器仓库中,您可以找到最终版本的代码,以防我们遗漏任何细节,您可以查看它。
小部件#
开始构建插件的最佳方式是首先在 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
,一个从QWidget
和SpyderWidgetMixin
派生的类,包含一个图标、一个标签和一个旋转器(用于显示插件加载)。BaseTimerStatus
,一个从StatusBarWidget
派生的类,带有一个内部QTimer
用于定期更新其内容。
注意
下面,我们将指出 github 中的链接,其中包含标签之间的差异,这有助于检查代码中将进行的渐进式更改。
我们将在第一次编辑后达到的第一个版本将被称为 HELLO WORLD
。
INITIAL -> HELLO WORLD widgets.py 差异
由于我们想要一个显示番茄工作法倒计时并定期更新的小部件,我们将使用 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 需要为 BaseTimerStatus
定义 ID
。它的构造函数调用父类构造函数并用 value
初始化标签。
我们添加了一个工具提示来验证我们小部件的存在。由于 Spyder 使用 qtawesome
(我们的另一个项目,简化了在 PyQt 应用程序中集成图标字体),我们可以通过在终端运行 qta-browser
命令来选择一个合适的图标。
(spyder-dev) $ qta-browser
从这里我们可以选择并复制我们偏好的图标名称。

为了完成我们小部件的实现,我们需要添加以下方法:
# ---- BaseTimerStatus API
def get_value(self):
"""Get current time of the timer"""
return self.value
BaseTimerStatus
要求实现此方法,以便每次内部计时器请求时更新其内容。
容器#
我们插件开发的下一步是创建我们上面编写的小部件的一个实例,这样我们就可以把它添加到 Spyder 的状态栏中。为此,我们需要使用一个容器。由于 Qt 的特殊性,我们需要一个 QWidget
实例(即容器)作为我们插件其他所有小部件的父级(如上所述)。
因此,container.py
的 COOKIECUTTER 版本是:
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 差异
在这种情况下,SpyderPomodoroTimerContainer
已经定义,我们必须实现 setup
和 update_actions
方法。
现在我们将之前创建的小部件添加到容器中。为此,首先我们需要导入该小部件。
# Local imports
from spyder_pomodoro_timer.spyder.widgets import PomodoroTimerStatus
然后我们编辑 setup
方法以添加我们小部件的一个实例。
def setup(self):
# Widgets
self.pomodoro_timer_status = PomodoroTimerStatus(self)
插件#
最后,我们定义我们的插件,以便它在 Spyder 中注册。plugin.py
的 INITIAL 版本(即由 cookiecutter 创建的版本)是:
导入
# 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 on_initialize(self):
container = self.get_container()
print('SpyderPomodoroTimer initialized!')
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 差异
首先,我们需要通过定义 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
并在 on_initialize
方法之后添加以下内容:
@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
就可以像访问自己的属性一样访问 SpyderPomodoroTimerContainer
的 pomodoro_timer_status
。
总而言之,我们做了以下工作:

我们创建了一个小部件,然后将其添加到容器中,该容器通过 CONTAINER_CLASS
常量在插件中注册。在插件中,我们访问了该小部件的实例并将其添加到状态栏中。
如何测试我们的插件#
现在是时候看看我们的插件在 Spyder 界面中是什么样子了。
从我们插件的根文件夹中,激活安装 Spyder 的环境,然后运行:
(base) $ conda activate spyder-dev
(spyder-dev) $ pip install -e .
现在我们可以看到两个输出。第一个显示在命令行中:
(spyder-dev) $ spyder
SpyderPomodoroTimer registered!
在 Spyder 中,您将在状态栏中看到我们的插件,工具提示为“I am the Pomodoro tooltip”。

请记住,每次我们更改代码时,都需要重新启动 Spyder,以便重新加载插件并检查更改。
增强我们的插件#
从现在开始,我们将深入探讨 Qt 的实现细节。如果您有任何疑问,Qt 文档将是您最好的指南。我们为本次研讨会创建了一个附录,快速解释了 Qt 的基本概念,供那些急于了解的人参考:Qt 基础
计时器更新#
我们插件的第一个问题是它的番茄工作法计时器没有更新。要激活它,我们可以使用 PomodoroTimerStatus
中的 QTimer
,它之所以存在是因为它是 BaseTimerStatus
的一个实例。
状态栏中值更新的第二个版本名为 TIMER
。
回到 widgets.py
,并在导入行下方(第 22 行)添加此常量。
HELLO WORLD -> TIMER widgets.py 差异
# --- Constants
# ------ Time limits by default
POMODORO_DEFAULT = 25 * 60 * 1000 # 25 mins in milliseconds
INTERVAL = 1000
POMODORO_DEFAULT
用于设置番茄工作法时间的毫秒限制,INTERVAL
用于设置计时器更新速率。
现在,在 PomodoroTimerStatus
的 __init__
方法中,我们需要添加:
# 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 秒(一千毫秒)。
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,我们会发现我们的计时器已经启动。

计时器控制#
现在我们需要一种控制计时器的方法。我们可以通过向 Spyder 的工具栏添加一些按钮来实现,这将有助于学习如何在 Spyder 中使用工具栏、菜单和动作。
PomodoroTimerToolbar#
下一个将操作添加到工具栏的版本称为 ACTIONS
。
TIMER -> ACTIONS widgets.py 差异
让我们回到 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
中,就使用它!
接下来,我们需要在我们的状态小部件中声明一个布尔变量,以指示倒计时是否暂停。为此,请在 PomodoroTimerStatus
的 __init__
方法中添加以下内容:
self.pause = True
并在 update_timer
方法内部,替换:
if self.countdown > 0:
...
为:
if self.countdown > 0 and not self.pause:
...
创建番茄工作法工具栏#
现在我们将在工具栏中创建一个新部分,并通过动作将一些功能与之关联。这些特定信息建议包含在 api.py
文件中,因为这样我们可以为 Spyder 的其余部分和新插件提供端点,以调整我们插件的行为。
让我们在 api.py
的末尾添加以下内容:
class PomodoroToolbarActions:
Start = 'start_timer'
Pause = 'pause_timer'
Stop = 'stop_timer'
class PomodoroToolbarSections:
Controls = "pomodoro_timer"
class PomodoroMenuSections:
Main = "main_section"
通过这些,我们正在告诉 Spyder 的其余部分以及我们自己的插件,我们将有一个名为“pomodoro_timer”的新工具栏部分。该部分将包含一个带菜单(仅含一个“main_section”部分)的按钮,以及标识为“start_timer”、“pause_timer”和“stop_timer”的动作,分别用于启动、暂停和停止(重置)我们的计时器。
请注意,这些是简单的类定义,带有类常量,以便以简单的方式封装和交换此信息。
将动作添加到工具栏#
TIMER -> ACTIONS container.py 差异
现在我们转到 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,
)
接下来,我们需要在 SpyderPomodoroTimerContainer
的 setup
方法中完成以下事情。
第一步是创建我们之前声明的工具栏类的一个实例:
title = _("Pomodoro Timer Toolbar")
self.pomodoro_timer_toolbar = PomodoroTimerToolbar(self, title)
第二步是创建与开始、暂停和停止番茄计时器相对应的动作。
# 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
声明的末尾,在我们的 cookiecutter 模板指示为 # --- 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
这些方法只是操作 pomodoro_timer_status
的 pause
字段,而对于 stop_pomodoro_timer
,倒计时会重新启动。
注册工具栏#
最后强制性的一步是转到 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)
查看更改#
我们首先注意到的是,工具栏中已经有了相应的按钮。

在创建动作时作为 tip
参数输入的字符串会在这里显示为按钮的工具提示。
此外,如果我们检查“视图 > 工具栏”菜单,我们会发现那里有一个新的条目,对应于我们的工具栏。

最后,让我们检查一下工具栏中新的番茄工作法计时器控制按钮如何与状态栏中的组件进行交互。

添加配置页面#
Spyder 插件的另一个特性是它们可以拥有出现在 Spyder 偏好设置窗口中的可配置选项。
配置默认值#
我们添加可配置参数的最终版本将被称为 CONFPAGE
。
第一步是定义我们希望提供给用户的选项。为此,我们必须创建一个新文件,可以命名为 conf.py
。在此文件中,我们将写入以下内容:
ACTIONS -> CONFPAGE config.py 差异
"""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
的声明,它是 Preferences 中对应我们插件部分的内部名称;以及与 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.py
移动到 conf.py
,因为我们现在有一个专门用于默认配置值的地方。
配置页面#
现在,我们需要构建将出现在“偏好设置”窗口中的页面。为此,我们按照以下方式编辑由 cokkiecutter 生成的 confpage.py
文件:
ACTIONS -> CONFPAGE confpage.py 差异
"""
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
允许一步实例化和初始化相应的前缀和后缀标签以及微调框的小部件。
传播配置更改#
由于我们将所有配置信息移动到了 conf.py
,现在我们必须从那里将其导入到 widgets.py
中。
ACTIONS -> CONFPAGE widgets.py 差异
# 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()
注册偏好设置#
最后,有必要通过要求 Preferences 插件来激活 plugin.py
中偏好设置的使用。
ACTIONS -> CONFPAGE plugin.py 差异
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)
现在我们可以从工具栏或“工具 > 偏好设置”菜单访问“偏好设置”窗口。在那里,我们将找到一个名为 Spyder Pomodoro Timer 的部分,其中包含 Pomodoro timer limit 参数。如果更改该值,我们将看到状态栏中相应的标签发生变化。

现在您的插件已是初始版本,准备发布…
发布您的插件#
由于安装 Spyder 的推荐方式是通过 conda,显而易见的选择是将我们的插件通过 conda-forge 等通道发布,但这超出了本次研讨会的范围,因为它过于复杂。
然而,用于在 conda 中发布包的工具通常基于在 PyPI 中发布的包。所以让我们看看如何在 PyPI 中发布我们的插件。

PyPI 和 TestPyPI#
我们要做的第一件事是在 PyPI 和 TestPyPI 网站上创建一个账户。虽然我们的包最终将在 PyPI 上发布,但建议使用 TestPyPI 来测试我们的包能否正确发布,而不会给 PyPI 服务器造成额外负载或影响其日志。
接下来,我们需要用我们自己的数据编辑项目根目录下的 setup.py
文件。幸运的是,cookiecutter 已经为我们创建了一个。
要将我们的包上传到 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
。
为了解决第一个问题,我们可以按照“制作一个 PyPI 友好的 README”中的说明进行操作。由于我们已经有一个 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.ac.cn/classifiers。在这里,我们可以简单地复制我们认为合适的分类器,然后将它们粘贴到我们的代码中。具体来说,在 setup.py
中,在调用 setup
函数时作为 classifier
参数传入的列表中。
进行这些更改后,并通过在 spyder_pomodoro_timer
文件夹内的 __init__.py
文件中提高我们插件的版本,我们可以重复构建新版本包、将其加载到 TestPyPI 进行检查,最后通过使用以下命令将其加载到 PyPI 的循环:
$ twine upload dist/
并在 https://pypi.ac.cn/project/spyder-pomodoro-timer/ 查看结果。
一旦完成,任何人都可以通过运行以下命令在他们的环境中安装我们的插件:
$ pip install spyder-pomodoro-timer
结束语#
通过插件、扩展或附加组件(通常这样称呼)使工具具有可扩展性,这是一项基本功能,它允许利用第三方开发人员的才能来响应应用程序核心开发团队范围之外的需求和增强功能。
同样,基于插件的系统使得应用程序的维护变得更加容易。最终,启用和禁用插件的能力使其更能适应不同的用例。例如,目前很难想象一个网络浏览器没有用于阻止广告或组织链接的扩展,即使这些功能并非默认提供。
在 Spyder 中,我们特别关注巩固一个允许以一致方式开发插件的 API。第 4 版和第 5 版之间的开发工作的重点就在这个方向,我们正处于一个关键时刻,期待利用所有这些工作成果。
在本工作坊中,您已学会如何
识别 Spyder 开发中的基本构建模块。
识别可以在 Spyder 中实现的不同类型的插件。
识别 Spyder 中包含的插件类型。
规划新的 Spyder 插件的开发。
为 Spyder 插件开发构建开发环境。
使用 Cookiecutter 生成 Spyder 插件的基本结构。
了解 Spyder 插件的文件结构。
在 Spyder 状态栏中添加并注册 Qt 小部件。
在 Spyder 工具栏中添加并注册 Qt 小部件。
在工具栏中添加带有动作的菜单。
为我们的插件添加配置选项并将其显示在“偏好设置”窗口中。
编辑我们插件的可安装包的描述和分类器。
将我们的插件发布到 TestPyPI 和 PyPI。
我们希望这些技能能帮助您更轻松地开发自己的 Spyder 插件。
如果您有插件开发的想法,请随时通过 Spyder-IDE Github 组织空间联系我们。
如果您对使用 Spyder 进行科学计算的入门感兴趣,可以访问研讨会使用 Spyder 进行科学计算和可视化。
如果您对使用 Spyder 进行金融数据分析的入门感兴趣,可以访问研讨会使用 Spyder 进行金融数据分析。
作业#
正如您可能已经注意到的,还有一些功能需要实现,例如番茄工作法完成时的通知。尝试实现它们,如果您有任何疑问,请随时联系我们。
延伸阅读#
在 plugin-examples 仓库中,您可以找到更多示例,它们肯定会对您进一步理解 Spyder 插件开发有所帮助。
更深入地回顾 Spyder 仓库本身,特别是其更简单的插件,例如 History、Plots 或 Working directory,可能会帮助您更好地理解它。以及回顾 spyder.api
中存在的各种辅助函数、小部件和混入。