Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature: 推广基于 PEP 420 的命名空间插件 #535

Closed
shniubobo opened this issue Sep 23, 2021 · 25 comments
Closed

Feature: 推广基于 PEP 420 的命名空间插件 #535

shniubobo opened this issue Sep 23, 2021 · 25 comments
Labels
enhancement New feature or request good first issue Good for newcomers

Comments

@shniubobo
Copy link

现状

目前插件若通过 nonebot.load_pluginnonebot.load_plugins 加载,可能会导致出现这种代码:

nonebot.load_plugin('nonebot_plugin_status')
nonebot.load_plugin('nonebot_plugin_apscheduler')
nonebot.load_plugin('nonebot_plugin_picsearcher')
nonebot.load_plugin('nonebot_plugin_translator')
nonebot.load_plugin('nonebot_plugin_cooldown')
...
nonebot.load_plugins('plugins')

其中存在这样几个问题:

  1. 重复代码
  2. 插件名字里的下划线很丑!
  3. 在每次新安装插件后都需要相应地修改代码,无法自动发现新安装的插件

其中第一项虽然可以通过 nonebot.load_all_plugins, nonebot.load_from_jsonnonebot.load_from_toml 解决,但剩余问题仍然存在。

可能的解决方案

插件作者方面

依照 PEP 420 修改插件文件布局。例如:

pyproject.toml
nonebot/
    plugins/
        foo.py
        bar.py

框架方面

若需解决上述第二个问题

插件作者修改文件布局后,插件可以在通过 pip 安装后直接使用目前的 nonebot.load_builtin_plugins 导入,例如:

nonebot.load_builtin_plugins('foo')
nonebot.load_builtin_plugins('bar')

考虑到函数名的问题,以及多次调用同一函数带来的不便,可以定义以下函数:

def load_namespace_plugins(names: list[str]) -> list[Plugin]:
    loaded_plugins = [load_builtin_plugins(name) for name in names]
    return loaded_plugins

若需进一步解决上述第三个问题

通过 pkgutil.iter_modules 等函数获得 nonebot.plugins 下的所有模块,并自动加载。

该解决方案的缺点

  1. 由于目前已有数十个插件,向命名空间插件迁移需要一定的时间与人力成本
  2. 需要同时支持新旧两种风格的插件,需要额外的维护成本
  3. 没有养成创建虚拟环境的好习惯的人可能会意外加载不需要的插件

其中第一项随着时间的推移可以慢慢解决,且迁移仅需更改文件夹结构,难度较小。第二项难以避免,用户自行编写而没有发布的插件使用旧风格加载较为方便。第三项不需要考虑,为用户自身的问题;或者提供开关,控制是否自动加载。

参考


请项目维护者们考虑该提议是否可行,谢谢!

@shniubobo shniubobo added the enhancement New feature or request label Sep 23, 2021
@mnixry mnixry added the good first issue Good for newcomers label Sep 23, 2021
@yanyongyu
Copy link
Member

yanyongyu commented Sep 23, 2021

可以参阅

https://v2.nonebot.dev/api/plugin.html#load-from-json-file-path-encoding-utf-8

https://v2.nonebot.dev/api/plugin.html#load-from-toml-file-path-encoding-utf-8

从配置文件加载插件

至于namespace这个暂时不会对用户开放,由于pip的包管理能力较差,容易导致混乱

下划线丑

建议和guido互怼

@shniubobo
Copy link
Author

可以参阅

https://v2.nonebot.dev/api/plugin.html#load-from-json-file-path-encoding-utf-8

https://v2.nonebot.dev/api/plugin.html#load-from-toml-file-path-encoding-utf-8

从配置文件加载插件

这两个函数我有提到:

其中第一项虽然可以通过 nonebot.load_all_plugins, nonebot.load_from_jsonnonebot.load_from_toml 解决,但剩余问题仍然存在。

@shniubobo
Copy link
Author

下划线丑

建议和guido互怼

不喜欢下划线的不止我,还有 PEP 8

@StarHeartHunt
Copy link
Member

  1. 在每次新安装插件后都需要相应地修改代码,无法自动发现新安装的插件

使用 nb-cli 安装会自动修改配置文件,添加新插件包。

@yanyongyu
Copy link
Member

顺便还有个问题

插件作者为什么要写成namespace插件,目前看来更多的是将自用插件发布到商店,然而namespace插件需要新建一个包结构,并且发布,相比目前的发布流程更复杂。

@yanyongyu
Copy link
Member

yanyongyu commented Sep 23, 2021

另外自动发现插件这个问题

你可以看看nonebot.plugin.manager
使用pkgutils自动发现插件是个非常麻烦的事,插件间无法互相import,无法加载子插件,当时就是因为非常多的用户需要插件间访问所以只能使用import hook进行patch

@j1g5awi
Copy link
Member

j1g5awi commented Sep 23, 2021

就算采用了此规范,PyPI 的包名也仍然是 nonebot_plugin_* 的形式,你在安装依赖的时候还是会看到很丑的下划线。

@yanyongyu
Copy link
Member

yanyongyu commented Sep 23, 2021

namespace问题+1:插件间冲突,插件编写混乱

namespace存在文件覆盖问题

怎么能把流氓放进家里

无法保证插件质量与规范的情况下不会这么干

当然你也可以强行这么干

@yanyongyu
Copy link
Member

每次安装插件后都需要修改代码

立即使用nb-cli

@yanyongyu
Copy link
Member

yanyongyu commented Sep 23, 2021

插件名也没强制你使用nonebot_plugin_xxxx啊,商店里不就有叫其他名字的

Underscores can be used in the module name if it improves readability.

nonebot_plugin_ 前缀仅是为了方便人识别这个包是nonebot插件

@stdrc
Copy link
Member

stdrc commented Sep 23, 2021

是非常好的建议,非常感谢你花精力考虑改进 NoneBot 2,不过我个人认为

  • 由于目前已有数十个插件,向命名空间插件迁移需要一定的时间与人力成本
  • 需要同时支持新旧两种风格的插件,需要额外的维护成本
  • 没有养成创建虚拟环境的好习惯的人可能会意外加载不需要的插件

这些缺点确实相比带来的“遵守 PEP”这一个好处,成本还是有点高了。

有时候为了遵循标准而引入过多维护或升级开销,并不是很值得的,对于 NoneBot 这样的项目,用起来直观是最重要的,要让用户(包括插件开发者)的心智负担最小。

至于其他讨论,看其他活跃开发者吧~

@yanyongyu
Copy link
Member

非常抱歉,由于在外面前面的评论没有仔细整理。

你的方案在nonebot2早期开发中已经考虑过相似问题并否决了,存在的问题还有不少,暂时无法对这部分问题作出改动

我认为暂时的解决方案是使用load_from_xxx从配置文件加载(或者使用load_all_plugins也行)

感谢你的建议!

@shniubobo
Copy link
Author

shniubobo commented Sep 23, 2021

使用 nb-cli 安装会自动修改配置文件,添加新插件包。

试了一下这个方法确实挺方便的。不过个人认为 nb-cli 干的事情实在太多了,这也是为什么我没有用它,甚至因此不知道这个功能;我个人更喜欢poetry那样只做依赖管理和构建的设计。需要考虑到有像我一样不使用 nb-cli 的人。


插件作者为什么要写成namespace插件,目前看来更多的是将自用插件发布到商店,然而namespace插件需要新建一个包结构,并且发布,相比目前的发布流程更复杂。

写成 namespace 插件的原因我已经说过了。

发布流程更复杂的确也是一个缺点,但同时也带来了安装方便的优点。为什么会有这么多包管理器?明明开发者发布时需要考虑各个包管理器的安装,很麻烦,为什么不直接把源码发出来,让用户下载下来编译一下直接用?答案就是要方便用户。我的这个提议也是相同的道理。

此外,目前已经有许多插件发布在了 PyPI 上。我没有找到发布插件相关的文档,不清楚在不发包的情况下、是否同样能够发布插件。如果答案是否定的,那么也就没有了复杂这一说,只需要新建一两个文件夹,移动一下文件就行了。


使用pkgutils自动发现插件是个非常麻烦的事,插件间无法互相import,无法加载子插件,当时就是因为非常多的用户需要插件间访问所以只能使用import hook进行patch

这一问题的确没有考虑到。没有详细了解过这一问题,抱歉暂时无法给出解决方案。


就算采用了此规范,PyPI 的包名也仍然是 nonebot_plugin_* 的形式,你在安装依赖的时候还是会看到很丑的下划线。。。

完全可以采用 nonebot-plugin-* 的形式发布。当然也可以让用户自行选择包名,不给任何规定。

插件名也没强制你使用nonebot_plugin_xxxx啊,商店里不就有叫其他名字的

我也不清楚规定是什么,因为发布插件的文档目前是空的。但是目前的事实是,大多数插件都是如此命名的,在插件名前加上这个前缀已经是不成文的规定了。


namespace存在文件覆盖问题

如果是要避免意外的覆盖,可以要求每个插件需要有各自的目录,并且规定名字不能重复。

但如果是要避免故意的覆盖,或许可以想办法解决。如果各位开发者愿意,我们可以进一步讨论研究一下这个问题。


这些缺点确实相比带来的“遵守 PEP”这一个好处,成本还是有点高了。

好处并不是遵守 PEP,而是解决我提出的后两个问题;在我看来,对于这一 PEP 甚至没有遵不遵守一说——它只是提供了一个新功能而已。我引用 PEP 的目的仅仅是为了说明这一方法有 python 的语言特性支持,而这也是我提议这种方式、而不是其他方式的原因。

对于 NoneBot 这样的项目,用起来直观是最重要的,要让用户(包括插件开发者)的心智负担最小。

我提议的这一改变虽然会加大插件开发者的负担,但会减少插件使用者的负担。参见前面的和包管理器的类比。


在我忙着回复的同时各位似乎已经定好了不采纳的结论。如果在看完我的回复后依然选择不采纳我会关闭 issue,但如果希望继续讨论、尝试解决各位指出的、这个提议的几个问题的话,我也愿意继续下去。

@yanyongyu
Copy link
Member

pypi发布的插件都是nonebot-plugin-xxx,然而import必须使用nonebot_plugin_xxx

你见过python import的时候使用-的嘛:joy:

@shniubobo
Copy link
Author

shniubobo commented Sep 23, 2021

pypi发布的插件都是nonebot-plugin-xxx,然而import必须使用nonebot_plugin_xxx

你见过python import的时候使用-的嘛😂

看你把之前那条删掉了,就没有回复这一点。

这里并没有涉及到 import,用户要么通过函数加载插件,要么按照提议自动加载插件,不需要考虑这一点。

Edit: 还是说你的意思是,即便是通过函数加载,也会遇到同样的问题?这样的话一个方法是直接自动加载,这样包名的问题就不再考虑,有下划线也无所谓;或者尝试让函数转换;或者放弃解决这一问题。

不过我本来也是没有说清楚,说下划线难看的意思是现在包名前面的前缀太难看了,并不是要针对下划线本身。

@yanyongyu
Copy link
Member

yanyongyu commented Sep 23, 2021

前面你发的pep8就规定了只能使用下划线

另外是为了保证插件可以被正常方式import

@shniubobo
Copy link
Author

前面你发的pep8就规定了只能使用下划线

并没有这种规定,见下方解释。

另外是为了保证插件可以被正常方式import

就拿 nb-cli 做例子:

$ pip install nb-cli
import nb_cli

下载时用的名字和导入时用的是两个不同的名字;插件名字里带有减号并不影响导入。同样见下方解释。


首先需要了解 module 和 package 的区别。假设有如下文件:

foo/
    __init__.py
    bar.py

那么 foo 是一个 package,而 foo.bar 是一个 module。运行时,导入后二者没有区别,使用 type 检查可以发现都为 module(这里有一个更为详细的回答)。

由此可以发现插件属于 package,而 "Python packages should also have short, all-lowercase names, although the use of underscores is discouraged",因此在条件允许时,应尽力避免下划线。

其次,需要分清代码中 package 的名字,和 PyPI 上 package 的名字。前者适用 PEP 8,且需要遵守 python 语法(也就是说它必须是一个合法的标识符,像减号肯定就是不允许的);后者适用 PEP 508,不需要遵守 python 语法(请尝试运行 pip install nb._-cli)。

这两个名字不一定需要一样,例如在前者是 foo 的情况下,后者可以为 mY.aweSome-FOO(虽然一般要求二者名称尽量一致)。前者由文件夹的名字决定,导入的时候需要使用前者;后者由 setuptools.setupname 参数决定(使用 poetry 时由 pyproject.toml 中的 tool.poetry.name 属性决定),使用 pip 安装时使用后者。

@yanyongyu
Copy link
Member

yanyongyu commented Sep 23, 2021

我再明确说一遍

  1. 根据pep8规范,module_name只能使用下划线
  2. pip安装使用的是project name你当我不知道?请你去pypi.org官网尝试搜索带.这种符号的project,很遗憾,我也进行过尝试,带有.符号的包无法被搜索到,即便PEP508允许,但大家的习惯就是使用-_
  3. 插件同时支持package和module形式
  4. nonebot2使用的project name是nonebot2,但package name是nonebot,nonebot2也使用的poetry进行包管理
  5. 我自认为我对python package的理解透彻,你说的我也都知道,你来操作一下import hook并阅读importlib源码你也可以
  6. 我并不认为插件应该去除nonebot-plugin-前缀,这是为了方便人识别。当然你完全可以自己取个另外的名字,没人拦着你。
  7. 你给出的例子中 foo 是 package,因为他的文件夹下存在__init__.py__init__.py 文件被执行并且作为 foo module插入到 sys.modules 缓存
  8. 插件带有-确实不影响importlib.import_module,但请问你能否使用import关键字进行import呢?如果用户存在插件间交互,无法使用import关键字将会是非常差的体验

最后

  1. 我是真越来越搞不懂你的需求了,你最后一个回复都在说啥?你要什么?
  2. 你没必要解释这些基础的东西
  3. 请直接说明白你的需求

插件改名又没人拦着你,你自己改个名字不行嘛 下划线换成短横或者去掉不就行了吗

@yanyongyu
Copy link
Member

yanyongyu commented Sep 23, 2021

另外开发团队群里指出的其他问题

他这个搞法不就是把nb2的插件改回nb1的丢进某个目录然后直接load目录吗
那我们还搞屁的pypi package?

我手动load还能控制是否加载
如果你第三方插件都整进我目录了
我某些环境不想加载怎么办呢?
TUJN(DZKXDNN@$01IT{2NII

难道开发者直接发布源码,用户去一个一个源码下载,丢进自己的目录就不是麻烦用户吗
其实是想把所有的插件放到一起
放到一个目录里
这首先违反了 pypi package 的组织模式啊

poetry 修改了你的 pyproject 配置文件, nb-cli就不能修改 pyproject 配置文件?

我始终认为,命名空间包本质上是一种懒惰的模块构造方式
这种构造方式应该且仅应该用于子模块是主模块的完全从属关系,且整体的开发是统一规划的

↑包括我之前所提到的 namespace 文件覆盖问题,也是我认为 namespace 不应该向用户开放的原因

那说白了就是懒得添加 load_plugin,他很有可能没考虑过已安装的插件选择不加载的情况
主要是不用 nb-cli 的他不想自己加 load_plugin

@XZhouQD
Copy link
Contributor

XZhouQD commented Sep 23, 2021

PEP 420提供的是,package定义来自于上层(主包,如nonebot),而所有下层目录以子模块形式出现,发布时依赖于主模块的package定义(如nonebot.plugin.help, nonebot.plugin.test, nonebot.plugin.docs,他们的发布均需要依赖于nonebot2的模块定义),且这些子模块不是真正的完整package,目前无法被pypi搜索。

PEP 420更适合于:子模块的开发有统一的管理,使用者知晓每一个子模块,所有子模块从属于主模块,主模块甚至根据子模块的出现而变化自身的功能实现。

PEP 420不适合于:子模块的开发来自于社区分别开发,使用者需要查询子模块,子模块与主模块之间具有相对较大的独立性。

对于这种子模块插件的开发,由于发布时必须依赖主模块的定义,容易造成命名空间污染。

此外,由于这类子模块无法控制加载顺序,无法实现子模块互相依赖,必须提前获知子模块之间的依赖,本质上并不方便使用者。

由于命名空间的共享问题,是会出现如下情形的:

# foo和bar两个包都在path内
# foo.name.first, bar.name.second为两个函数
import name.first
import name.second
# 正常生效,均import成功
# 此时,name空间实际上混合了foo.name和bar.name的内容,危险!

@Lancercmd
Copy link
Contributor

@shniubobo 不可否认的是,nonebot2 相关文档的缺失,大伙都是看在眼里的

但是看起来目前本 Issue 的发展和最初的提问及建议存在出入,有一些应于现在明确的观点

作为同样不使用 nb-cli 的 nonebot2 项目和插件开发者,我认为

  • 我不明白为什么要把导入不同插件的行为按重复代码来归类,你不总是需要导入所有已安装的插件

    更具有攻击性的说法,比起在启动脚本上按行收费,不如在业务代码层面着重优化

    是不是用 for 循环的 nonebot.load_plugin() 就显得不这么重复了呢?

  • 毫无疑问的,包名漂亮与否不是决定是否应改变长期使用的标准命名的因素

  • “对于因为每安装新的插件需要手动添加 nonebot.load_plugin() 很麻烦,所以应作出改变” 这个理由是完全站不住脚的

    nb-cli 目前的角色,比起人人都应使用的 nonebot2 脚手架,
    更接近于是新手上路时,生成第一个 nonebot2 项目才会去使用的东西。
    并且很多人可能对于 nb-cli 的功能、使用场景和限制都了解尚浅,
    nb-cli 在开发者用户的使用过程中有时候甚至会成为生成项目的阻碍。
    虽然它不能算非常好用,但是不能因为你不用,就无视这个解决方案。

没有养成创建虚拟环境的好习惯的人可能会意外加载不需要的插件

  • 意外的是,其实有相当一部分开发者用户,不会去使用虚拟环境,设计功能或插件也应积极地以不使用虚拟环境为前提

另外,本 Issue 目前的发展多围绕 PEP 标准进行讨论,表示理解但又没有完全理解。

因为我不明白的是,在您看来,命名空间到底能为 nonebot2 带来什么样的优势呢?

@shniubobo
Copy link
Author

PEP 420更适合于:子模块的开发有统一的管理,使用者知晓每一个子模块,所有子模块从属于主模块,主模块甚至根据子模块的出现而变化自身的功能实现。

PEP 420不适合于:子模块的开发来自于社区分别开发,使用者需要查询子模块,子模块与主模块之间具有相对较大的独立性。

对于这种子模块插件的开发,由于发布时必须依赖主模块的定义,容易造成命名空间污染。

很有道理,子模块并不特别适合 nonebot 的场景。

由于命名空间的共享问题,是会出现如下情形的:

对于这一问题我先前有提出解决方法:“如果是要避免意外的覆盖,可以要求每个插件需要有各自的目录,并且规定名字不能重复。”具体来说规定这样的目录:

# plugin-foo
nonebot/
    plugins/
        plugin_foo/
            __init__.py
            name/
                __init__.py
                first.py

# plugin-bar
nonebot/
    plugins/
        plugin_bar/
            __init__.py
            name/
                __init__.py
                second.py
from nonebot.plugins import plugin_foo
from nonebot.plugins import plugin_bar
# plugin_foo.name.first 和 plugin_bar.name.second 没有混合

我不明白为什么要把导入不同插件的行为按重复代码来归类,你不总是需要导入所有已安装的插件

意外的是,其实有相当一部分开发者用户,不会去使用虚拟环境,设计功能或插件也应积极地以不使用虚拟环境为前提

这同时涉及到第一和第三个问题。我提出的第一个问题并不是这一提议需要解决的重点,并且我也已经列出了现有的解决方案,这一问题几乎可以忽视。至于第三个问题,这可能和我的个人习惯有关。我习惯使用 requirements.txt 或 poetry 来定义环境,环境内有的东西都是需要的东西,一旦不再需要就从环境中移除。换句话说,就是需要用插件就 poetry add,不再需要了就 poetry remove。手动加载这一行为本身对我来说就是多余、重复的,因为对我来说机器人能看到的就是我需要它加载的,不需要我再额外告诉它哪些需要加载。

不过考虑到这仅仅是我个人的习惯,的确会有很多人不总是加载所有安装了的插件,因此自动加载所有插件的功能并不适合所有人,同时也可能并不符合 nonebot 的设计理念。不过我也有提出过解决方法:“或者提供开关,控制是否自动加载。”或者也可以定义一个函数,允许用户调用后自动加载所有已安装的插件。请问这两种解决方法是否可以接受?

毫无疑问的,包名漂亮与否不是决定是否应改变长期使用的标准命名的因素

这里我想确认一点,目前大多插件采用的前缀是否是标准命名?

此外,即便旧的名字难以改变,也可以通过更改标准,确保新的名字好看。一个例子:logging.getLogger 与 PEP 8 的推广并不冲突。

虽然它不能算非常好用,但是不能因为你不用,就无视这个解决方案。

解决方案确实存在,但既然某些人(包括我)不认为它好用,那这个方法在这些人眼里自然就称不上“解决方案”。他们并不是无视了这个解决方案;对于他们来说,这不是解决方案。当然如果这些人的想法与 nonebot 的设计理念相悖,并因此决定不再提供更多解决方案,我认为这也完全可以接受。

另外,本 Issue 目前的发展多围绕 PEP 标准进行讨论,表示理解但又没有完全理解。

我提到 PEP 的时候主要有这些原因:

  1. 方便阐述:如果 PEP 已经讲的很清楚了,我就不需要再重复。例如标题中的 PEP 420。
  2. 增加我的说服力:例如一开始我提出我个人不喜欢下划线,但是似乎我的好恶并没有说服力,就只能搬出 PEP 8。

命名空间到底能为 nonebot2 带来什么样的优势呢?

优势就是解决了我一开始提到的那三个问题之中的后两个。

不过既然你会这么问,那么我认为或许这在你的眼中并不能算上优势。从其他各位开发者的回复来看,或许这一提议的优势也是微乎其微,并不能超过它带来的劣势。每个人的想法有所不同我认为很正常,虽然在各位眼中似乎这两个问题甚至可能称不上问题,在我眼中这两个问题的解决会是极大的优势。

最后,既然一片反对,是否应该继续讨论下去,还是说直接驳回这一提议?

@yanyongyu
Copy link
Member

首先你先要把用户放在不可信的角度,你无法强制规定插件名称不同,也没有这个精力来审核每一个发布的插件。

nonebot-plugin-前缀是习惯而非规定

我更希望用户插件能更加简单方便,直观。

另外poetry也无法代表一切,你无法保证每个人的环境都没有被污染,新手可能都无法理解依赖管理

所以你就是想偷懒自动加载 你是不是有强迫症?

@j1g5awi
Copy link
Member

j1g5awi commented Sep 24, 2021

这里我想确认一点,目前大多插件采用的前缀是否是标准命名?

这是一种“约定俗成”,并不具有强制性。只要包名和项目名不与 PyPI 上的其他项目重复,怎么取都行。

此外,即便旧的名字难以改变,也可以通过更改标准,确保新的名字好看。一个例子:logging.getLogger 与 PEP 8 的推广并不冲突。

插件开发者无法知道用户的环境里会不会有一个名为 logging 的包,所以为了保险起见就给插件加上了 nonebot 的前缀。

前缀并不是为了美观,而是一种保险。

@shniubobo
Copy link
Author

插件开发者无法知道用户的环境里会不会有一个名为 logging 的包,所以为了保险起见就给插件加上了 nonebot 的前缀。

前缀并不是为了美观,而是一种保险。

需要明确一点,我从来没有认为需要去掉前缀,也没有认为前缀是为了美观。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request good first issue Good for newcomers
Development

No branches or pull requests

8 participants