从子模块导入 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
脚本的工作,在这个存储库中。
背景
我有一个 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
脚本的工作,在这个存储库中。