从子模块导入 Python 包时避免 pylint 投诉

Avoiding pylint complaints when importing Python packages from submodules

背景

我有一个 Python 应用程序依赖于作为 git 子模块提供的另一个包,产生类似于以下的目录结构:

foo/
    bar/
        bar/
            __init__.py
            eggs.py
        test/
        setup.py
    foo/
        __init__.py
        ham.py
    main.py

访问foo包很简单,因为main.py是从顶层foo/目录执行的;但是 bar 包嵌套在另一个 bar 目录中,不能直接导入。

这很容易解决,通过修改 main.py 开头的 sys.path:

import sys

# Or sys.path.append()
sys.path.insert(0, './bar')

from bar.eggs import Eggs
from foo.ham import Ham

(注意:此代码示例假定 main.py 将始终从 foo/ 调用;在可能不是这种情况的情况下,'.bar' 可以替换为 os.path.join(os.path.dirname(__file__), 'bar') 虽然这显然更笨重。)

问题

不幸的是,pylint 不喜欢这个解决方案。虽然代码有效,但 linter 将 sys.path 修改视为结束 "top of the module" 的代码块,并给出了不受欢迎的 wrong-import-position 警告:

C: 6, 0: Import "from bar.eggs import Eggs" should be placed at the top of the module (wrong-import-position)
C: 7, 0: Import "from foo.ham import Ham" should be placed at the top of the module (wrong-import-position)

类似问题

Adding a path to sys.path in python and pylint

这位提问者有一个问题,即 pylint 无法完全正确解析导入。这个问题的唯一答案是添加到 pylint 的内部路径;这无助于避免对交错 sys.path 修改的投诉。

配置pylint

禁用 .pylintrc 中的 wrong-import-position 检查器是最简单的解决方案,但会丢弃有效警告。

更好的解决方案是告诉 pylint 忽略这些导入的 wrong-import-position,内联。 false-positive 导入可以嵌套在 enable-disable 块中而不会丢失任何其他地方的覆盖:

import sys

sys.path.insert(0, './bar')

#pylint: disable=wrong-import-position

from bar.eggs import Eggs
from foo.ham import Ham

#pylint: enable=wrong-import-position

Ham()

# Still caught
import something_else

但是,如果 wrong-import-order.pylintrc 中被禁用,这确实有一点点时髦的缺点。


避免修改sys.path

有时不需要的 linting 警告源于错误地开始处理问题。我想出了很多方法来避免首先修改 sys.path,尽管它们不适用于我自己的情况。

也许最直接的方法是修改PYTHONPATH以包含子模块目录。然而,这必须在每次调用应用程序时指定,或者在 system/user 级别上修改,这可能会损害其他进程。该变量可以在包装 shell 或批处理脚本中设置,但这需要进一步的环境假设或限制对 Python.

调用的更改

一个更现代但更少trouble-fraught的模拟是在虚拟环境中安装应用程序,并简单地将子模块路径添加到虚拟环境中。

更进一步,如果子模块包含安装工具 setup.py,它可能只是被安装,完全避免了路径定制。这可以通过维护对存储库的发布来实现,例如 pypi(专有包的 non-starter)或通过 utilizing/abusing pip install -e 直接安装子模块包或从其存储库安装。再一次,虚拟环境通过避免潜在的 cross-application 冲突和权限问题使此解决方案更简单。

如果目标 OS 集可以限制为具有强大符号链接支持的那些(实际上这排除了所有 Windows 通过至少 10),子模块可以链接到绕过包装目录并将目标包直接放在工作目录下:

foo/
    bar/ --> bar_src/bar
    bar_src/
        bar/
            __init__.py
            eggs.py
        test/
        setup.py
    foo/
        __init__.py
        ham.py
    main.py

这会限制应用程序的潜在用户并使 foo 目录充满混乱,但在某些情况下可能是一个可以接受的解决方案。

硬编码位置

此设置的问题在于它对文件位置做出了非常的具体假设。特别是,它硬编码另一个包的位置。

在您的情况下,您将其硬编码为相对路径。这额外地要求最终用户拥有一个非常具体的当前目录。如果您是最终用户,这会很烦人。如果我有一个文件想用作您的代码的输入,我应该能够将我的当前目录作为我的用户主路径([=86= 中的~,[= 中的%USERPROFILE% 84=]) 并传入文件的相对路径,同时使用脚本本身的绝对路径。 (例如,python /path/to/your/script ./myinput.txt。)像这样的硬编码位置使它变得不可能。我还注意到您的 bar 目录包含一个 setup.py,暗示它是一个独立的包。精彩的。如果我想再次 运行 main.py 某个特定版本的软件包 installed 怎么办?同样,修改脚本执行的 sys.path 是不可能的。

您应该在代码中硬编码的唯一 位置是将使用代码直接 分发的资源位置,总是 在同一个位置,就像你在 eggs.py 旁边有一个 recipes.dat 文件一样。在这种情况下,路径应该相对于 脚本的 (或其他语言的二进制文件)的当前位置。 (例如,RECIPES_PATH = os.path.join(os.path.dirname(__name__), 'recipes.dat')。)当你有一个单独的包时,它可能位于你的 main.py 脚本预期的其他地方。

让Python发挥作用

查找和加载包是 Python 的一项基本功能。 让它那样做。 当您 运行 遇到无法开箱即用的情况(因为您的代码未安装在任何地方)时,请使用 使用它们的标准 机制。

PYTHONPATH 环境变量可能是处理它的最简单方法。这很容易。您只需要一个配套脚本来设置命令行环境:

setupenv.sh:

DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" # See 

if [ -n "$PYTHONPATH" ]; then
    PYTHONPATH=$PYTHONPATH:
fi
PYTHONPATH=$PYTHONPATH${DIR%%/}/bar

然后:

$ source setupenv.sh
$ python ./main.py

(在 Windows batch/cmd 文件中也同样简单。)

好的,在积极开发代码时,每次启动终端时都必须设置环境,有点 很烦人。但这还不错。我在自己的项目中这样做,这是我早上做的事情,在我启动新终端之前不会再考虑。 (除此之外,我的脚本还设置了更多:激活虚拟环境,为一些本机二进制文件设置 PATH。)它对项目来说非常非常干净。

您可能会争辩说:"Well, we're still hard coding the location in the sh file." 是的,我们是。但此脚本是 存储库 的一部分。请注意,我使用的路径是相对于脚本本身的;那是因为我知道代码存储库的结构。我不知道用户在命令行工作时的当前目录,我当然不知道 main.py 将分发到哪里。也许它会在最终目的地以自己的包裹结束。无论如何,了解其他包所在的位置不是该脚本的工作。 这个setupenv.sh脚本的工作,在这个存储库中。