背景
我有一个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
目录中,不能直接导入。
通过在sys.path
开头修改main.py
,这很容易解决:
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
修改是一个代码块,结束了“模块的顶部”并给出了一个不合需要的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
修改的投诉。
答案 0 :(得分:6)
配置pylint
在wrong-import-position
中禁用.pylintrc
检查程序是最简单的解决方案,但会抛弃有效的警告。
更好的解决方案是告诉pylint忽略这些导入的wrong-import-position
内联。误报导入可以嵌套在启用禁用块中,而不会丢失其他地方的任何覆盖:
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
以包含子模块目录。但是,每次调用应用程序或在系统/用户级别修改应用程序时都必须指定这一点,这可能会损害其他进程。该变量可以在包装shell或批处理脚本中设置,但这需要进一步的环境假设或限制对Python调用的更改。
更现代,更少麻烦的模拟是在虚拟环境中安装应用程序,只需将子模块路径添加到虚拟环境中。
到达更远的地方,如果子模块包含setuptools setup.py
,则可以简单地安装它,完全避免路径自定义。这可以通过将出版物维护到诸如pypi(非专有包的入门者)之类的存储库,或者通过利用/滥用pip install -e
直接安装子模块包或从其存储库来安装。虚拟环境再一次避免了潜在的跨应用程序冲突和权限问题,从而使此解决方案更加简单。
如果目标操作系统集可以限制为具有强大符号链接支持的那些(实际上这排除了所有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
目录的混乱,但在某些情况下可能是一个可接受的解决方案。
答案 1 :(得分:1)
此设置的问题在于它对文件的位置进行了非常特定的假设。特别是,它硬编码一个位置到另一个包。
在您的情况下,您将其硬编码为相对路径。这另外要求最终用户具有非常特定的当前目录。如果您是最终用户,这很烦人。如果我有一个文件我想用作代码的输入,我应该可以将当前目录作为我的用户主目录路径(Linux中为~
,Windows中为%USERPRPOFILE%
)并传入使用脚本本身的绝对路径时,我的文件的相对路径。 (例如,python /path/to/your/script ./myinput.txt
。)像这样的硬编码位置使得无法做到。我还注意到您的bar
目录包含setup.py
,这意味着它是一个独立的包。精彩。如果我想再次运行某个版本的安装的main.py
怎么办?同样,对于脚本执行sys.path
的修改,这是不可能的。
您应该在代码中硬编码的仅位置是资源的位置,这些资源将直接直接分发,代码始终相同的位置,就像recipes.dat
文件旁边有eggs.py
一样。在这种情况下,路径应该相对于脚本的(或其他语言中的二进制文件)当前位置。 (例如,RECIPES_PATH = os.path.join(os.path.dirname(__name__), 'recipes.dat')
。)当你有一个单独的包时,它可能在你的main.py
脚本所期望的其他地方。
查找和加载包是Python的基本功能。 让它做到这一点。当你遇到无法立即找到它的情况时(因为你的代码没有安装在任何地方),使用标准与他们合作的机制。
PYTHONPATH
环境变量可能是处理它的最简单方法。这很容易。您只需要一个配套脚本来设置命令行环境:
setupenv.sh
:
DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" # See https://stackoverflow.com/a/246128/1394393
if [ -n "$PYTHONPATH" ]; then
PYTHONPATH=$PYTHONPATH:
fi
PYTHONPATH=$PYTHONPATH${DIR%%/}/bar
然后:
$ source setupenv.sh
$ python ./main.py
(在Windows批处理/ cmd文件中执行此操作也同样简单。)
好的,当您积极开发代码时,每次启动终端时都必须设置环境,这是一个很小的。但它并没有那么糟糕。我在自己的项目中这样做,这是我早上做的事情,在我推出新终端之前不要再考虑了。 (我的脚本设置了更多:激活虚拟环境,为某些本机二进制文件设置PATH
。)对于项目来说,它更加清晰。
你可能会说,“好吧,我们仍然在sh文件中对位置进行硬编码。”是的我们是。但是这个脚本是存储库的一部分。请注意,我使用的路径是相对于脚本本身;那是因为我知道代码库是如何构建的。当他们在命令行工作时,我不知道用户的当前目录,我当然不知道将在哪里分发main.py
。也许它最终将在最终目的地的自己的包装中。无论如何,知道其他软件包所在的位置并不是该脚本的工作。 是此setupenv.sh
脚本的作业,在此存储库中。