menu

Tool Stack

lint

  • pylint
  • flake8
  • mypy

lsp 通过 publishDiagnostics 能代替或者通过插件的形式代替这些 lint

code style

  • pep8,https://pep8.org/

自动格式化工具

  • autopep8
  • black,争议大 uncompromising,不可配置,比如以下的强制行为:
    • 单引号转双引号
    • 空行
  • yapf, by google auto-formatting + beautify 可选择不同的 style guideline

lsp 通过 codeFormatting 支持格式化

lsp

支持各种 IDE feature,如:

Auto Completion Code Linting Signature Help Go to definition Hover Find References Document Symbols Document Formatting Code folding Multiple workspaces

补完通过传递 ~ CompletionItem~ 自动导入(autoImports) 通过 CompletionItem.additionalTextEdits 传递

工具

  • [ ] pyright
  • [ ] jedi
  • [X] pylsp

pyright

pyright 的定位是一个静态类型检测器,与之比较的是 mypy。不过它还带有一个实现 lsp 的服务 pyright-langserver

  • 支持自动导入,但初始化貌似没有加载 sys.path 的 module, 必须先 import os 后才行
  • 支持手动创建 stub 文件:pyright --createstub cv2
  • 对于额外的 .pyi 文件,比如 cv2 ,只需要存放到项目根目录的 typings 文件夹,便能优先识别:
typings
├── cv2
│   └── __init__.pyi

Jedi

pylsp

python-lsp-server 相当于一个实现 lsp 的脚手架,基础功能由 Jedi 提供,包括 Completions、Definitions、Hover、References、Signature Help 和 Symbols。 各个功能通过插件扩展实现。另外一些功能通过默认开启的插件提供,如 pycodestyle、pyflake 提供 diagnostic。

问题

  • 经常无响应,导致几乎不可用。估计是这个 ISSUE 描述的问题 https://github.com/python-lsp/python-lsp-server/issues/227

插件

  • 插件的发现机制: On startup, pylsp will automatically discover plugins by querying pkg_resources for entrypoints. 如:
    [options.entry_points]
    pylsp = pylsp_myplugin = pylsp_myplugin.plugin
        
  • 基于 pluggy

缺陷

  • 补全速度较慢

安装

为了用上最新的特性,clone 到本地再安装到虚拟环境

git clone https://github.com/python-lsp/python-lsp-server.git
pip install '.[all]' # 安装全部依赖

配置

  • rope
    • pylsp.plugins.rope_completion.enabled 默认不启用 completion,有 jedi 就够了,保持不启用,不开启也能使用 autoimport
    • pylsp.plugins.rope_autoimport.enabled 补全自动 import 需要开启
    • pylsp-rope,非自带插件 通过 code action 提供更多 rope refactor 的能力,比如
      • Extract method
      • Convert lacal variable to field
      • Organize import
  • mccabe 用于计算循环复杂度 默认开启
  • linter 默认是用 pycodestyle、pyflake,不支持 pyproject.toml 配置
  • formatter
  • pylsp-mypy 用于 lint,通过 pyproject.toml 配置 mypy 不能识别 `pip install –editable` 基于 setuptools 的包,见 #7508pyproject.toml 添加如下解决:
    [tool.setuptools]
    zip-safe = false #  force setuptools to always install the project as a directory. required by mypy(#7508)
        

Editor(Emacs)

venv 环境

  • 安装 python-lsp-server
  • pip install pyright black

lsp-bridge

环境要求:

  • pip install epc

elisp 模块与 python 模块 还有一个额外 acm 用于补全

langserver/*.json 的作用:

  • python 模块解析

-

Syntax

Shebang

不要写死 python 的路径,而是通过 env 获取 python 的路径:

python3:

#! /usr/bin/env python3

python2 或 python3:

#! /usr/bin/env python

Tips

  • 换行,在行尾加 \
  • try…except…else 写法

下划线变量

  • __all__

函数

参数默认值

  • 函数内修改参数默认值必须非常小心地完成。原因是第一次到达函数时,参数的默认值仅被执行一次。此后,相同的值(或可变对象)在后续函数调用中被引用。

Plugin

pluggy

一个 PluginManager 下有多个 hook,hook 由一个 spec 和多个 impl 组成。

hookspec

hookspec = pluggy.HookspecMarker("project_name")

hookspec 是一个 decorator,装饰一个函数相当于声明一个 spec。函数名相当于 spec 的签名,由于一个 hook 只有一个 spec 所以也相当于是 hook 的签名。也就是说同一个 PluginManager 下的 hookspec 不允许重名的。 具体的实现,是通过为被装饰函数添加一个属性来标识该函数是 spec

setattr(
              func,
              self.project_name + "_spec",
              dict(
                  firstresult=firstresult,
                  historic=historic,
                  warn_on_impl=warn_on_impl,
              ),
          )

hookspec 函数并不会被执行, pluggy 只会分析其函数签名。

hookimpl

hookimpl = pluggy.HookimplMarker("project_name")

hookspec 也是一个 decorator,作用与实现与 hookspec 类似。处理默认用函数名作为 hook 签名外,还能通过 specname 属性指定签名。

PluginManager

添加 hookspec(hookspec 等价于 hook):

pm.add_hookspecs(module_or_class)

遍历 module_or_class 的所有属性,找出其中被相同 project_name 的 hookspec 装饰的函数。接着对每个 hookspec 再其属性 pm.hook 创建一个相同签名的函数。

注册 hookimpl(相当于 plugin):

pm.register(module_or_class)

遍历 module_or_class 的所有属性,找出其中被相同 project_name 的 hookimpl 装饰的函数。添加到对应的 hook 中。如果找不到对应的 hookspec 会自动创建一个没有 hookspec 的 hook(HookCaller). 但是如果调用 pm.check_pending() 就会报错。通过设置 optionalhook 可以避免报错。

historic

@hookspec(historic=True)
def myhook(arg1, arg2):
    pass

标识可以在 register 之前,可以向改 hook 注册回调,每当一个新的 hookimpl 被注册,就是执行并将结果传给回调。

pm.hook.myhook.call_historic(lambda r: print(f"lambda:{r}"), {'arg1':1, 'arg2':2})

缺点

不支持条件触发。

entry_points

setuptools 也能通过配置 entry_points 实现插件的效果。

以为 plugin 的 pyproject.toml 为例:

[project.entry-points.{entry_point_group}]
{name} = {entry_point}
# 或者
[project.entry-points]
{entry_point_group} = [
n{name} = {entry_point},...
]

host 可以这样调用:

from importlib.metadata import entry_points
plugins_eps = entry_points(group=entry_point_group)
for ep in plugins_eps:
    plugin = ep.load()

entry point 的语法见 Entry Points - setuptools

Test

pytest

fixture

  • parameter matter! test 函数中的参数就是 fixture
  • fixture 的返回是会被缓存的[fn:1],由 scope 控制,默认是 function
    • 也就是说同一个 test 多次引用同一个 fixture 指向的都是同一个实例
    • 更大的 scope 更早执行,比如 session 比 class 早
  • autouse 标识所有 test 都依赖的 fixture 同一个 scope 下 autouse 最先执行
  • request 是 fixture 的特殊参数,一个对象,可以用于内省测试函数、类或模块上下文
    • 比如直接添加 teardown: request.addfinalizer(delete_user)
    • 或者通过@pytest.mark 为 fixture 提供数据
  • conftest.py,提供可供整个目录使用的 fixtures fixture 的搜索策略是当前再往上

parametrizing

对于每个参数都都生成一个新的方法

  • fixture 每个依赖以下 fixture 的 test 都会分别生成两个 test:
    • test[smtp.gmail.com]
    • test[mail.python.org]
@pytest.fixture(scope="module", params=["smtp.gmail.com", "mail.python.org"])
def smtp_connection(request):
  • test function @pytest.mark.parametrize.

Footnotes

[fn:1] https://docs.pytest.org/en/6.2.x/fixture.html#scope-sharing-fixtures-across-classes-modules-packages-or-session

keyboard_arrow_up