使用 --prefix 和 -e pip 安装本地包时的奇怪之处。 importlib.metadata 未正确遵循 .egg-link 到 .egg-info 的潜在错误
oddities when pip installing a local package with --prefix and -e. Potential bug with importlib.metadata not following .egg-link to .egg-info properly
Update 从各方面看来 python 3.8-3.10 没有遵循 .egg-link 或 easy-install.pth 存根正确获取 .egg-info 元数据。不知道为什么。尝试使用 brew 安装 python 3.10.1 并且 importlib.metadata 在 .egg-link 或 easy-install.pth 文件之后正确查找 .egg-info 元数据也有问题,尽管 .egg-link 和 easy-install.pth 在 $PYTHONPATH
背景: 我们工作的 CentOS 8 服务器安装了 python 3.6.8(使用 pip 9.0.3) .在工作时
一个项目,我们使用模块实用程序加载特定版本的程序,包括 python 3.8.3 (使用 pip 20.2.2)。在项目目录下是它自己的 bin/、lib/ 等。这允许我们将项目特定的 python 包安装到这些项目目录。其中有一个内部开发的包,我们使用该包在 console_scripts 入口点的帮助下管理我们的项目。这个内部开发的包在 git 的 VCS 下,可以在项目的生命周期内进行编辑。因此,在这个项目的上下文中工作时,我们希望能够编辑这个 python 包的源代码,同时将它安装在本地,以便可以使用它的控制台脚本.这只是 pip install --prefix project_dir -e pkg_src_dir
的用例
问题是,这适用于 python 3.6.8,但不适用于 python 3.8.3,这是我们实际用于项目的版本。而且我不确定它是否是 importlib.metadata 特定版本的错误,包括 python 3.8.3.
我创建了一个虚拟的 Hello World 包来尝试调试它。 mypkg.py 定义了一个打印 Hello World 的函数。 main.py 的 main() 函数调用了 mypkg 的 Hello World 打印函数。简单且此结构遵循python.org自己的打包教程。
mypkg/
├── setup.py
└── src/
└── mypkg/
├── __init__.py
├── mypkg.py
└── __main__.py
使用 python 3.6.8 及其 pip 9.0.3,pip install --prefix project_dir -e mypkg
的工作方式与您预期的一样。 project_dir/lib/python-3.6.8/site-packages
包含指向 mypkg/src
目录的 mypkg.egg-link
文件。在 project_dir/bin
中是 mypkg
控制台脚本。
#!/usr/bin/python3.6
# EASY-INSTALL-ENTRY-SCRIPT: 'mypkg','console_scripts','mypkg'
__requires__ = 'mypkg'
import re
import sys
from pkg_resources import load_entry_point
if __name__ == '__main__':
sys.argv[0] = re.sub(r'(-script\.pyw?|\.exe)?$', '', sys.argv[0])
sys.exit(
load_entry_point('mypkg', 'console_scripts', 'mypkg')()
)
通过将 project_dir/lib/python-3.6/site-packages
目录添加到 $PYTHONPATH
之前,我能够 运行 这个控制台脚本 mypkg
没有问题并让它打印 Hello World。我什至可以 运行 这个带有 python 3.8.3 的控制台脚本 运行 直接用 python 的那个版本 python, python-3.8.3 ./mypkg
。这是因为,正如我后来发现的那样,因为它使用的是 pkg_resources 中较旧的 load_entry_point 函数,而不是 importlib.metadata.
中的较新版本
但是,如果我尝试以完全相同的方式使用 python 3.8.3 安装相同的软件包,控制台脚本将无法 运行。这是在将 $PYTHONPATH
更新为 project_dir/lib/python-3.8/site-packages
之后。
Traceback (most recent call last):
File "./mypkg", line 33, in <module>
sys.exit(load_entry_point('mypkg', 'console_scripts', 'mypkg')())
File "./mypkg", line 22, in importlib_load_entry_point
for entry_point in distribution(dist_name).entry_points
File "/tools/conda/anaconda3/2020.07/lib/python3.8/importlib/metadata.py", line 504, in distribution
return Distribution.from_name(distribution_name)
File "/tools/conda/anaconda3/2020.07/lib/python3.8/importlib/metadata.py", line 177, in from_name
raise PackageNotFoundError(name)
importlib.metadata.PackageNotFoundError: mypkg
控制台脚本存根有很大不同,这些更改与使用 importlib.metadata 提供 load_entry_point 功能有关。
#!/tools/conda/anaconda3/2020.07/bin/python
# EASY-INSTALL-ENTRY-SCRIPT: 'mypkg','console_scripts','mypkg'
import re
import sys
# for compatibility with easy_install; see #2198
__requires__ = 'mypkg'
try:
from importlib.metadata import distribution
except ImportError:
try:
from importlib_metadata import distribution
except ImportError:
from pkg_resources import load_entry_point
def importlib_load_entry_point(spec, group, name):
dist_name, _, _ = spec.partition('==')
matches = (
entry_point
for entry_point in distribution(dist_name).entry_points
if entry_point.group == group and entry_point.name == name
)
return next(matches).load()
globals().setdefault('load_entry_point', importlib_load_entry_point)
if __name__ == '__main__':
sys.argv[0] = re.sub(r'(-script\.pyw?|\.exe)?$', '', sys.argv[0])
sys.exit(load_entry_point('mypkg', 'console_scripts', 'mypkg')())
有趣的是,当 运行 直接使用 python 3.6.8 二进制文件时,此控制台脚本可以正常工作。 编辑:这是有道理的,因为它回退到加载旧的 pkg_resources 版本的 load_entry_point,因为围绕导入的所有尝试 尽管有两个安装在 sys.path 搜索中共享相同的本地路径(即 project_dir/lib/python-3.8/site-packages
)。只有他们的 system/installation 特定路径不同,其中本地 mypkg
应该找不到。
我还发现,如果我将 python 3.6.8 控制台脚本中的 from pkg_resources import load_entry_point
行添加到 python 3.8.3 控制台脚本,当 运行 使用 python 3.8.3 时,我不再收到错误。 编辑:这又是完全有道理的,因为问题的根源与 importlib.metadata
有关
这是我的 setup.py
的完整披露。我不确定是否可以添加一些东西来解决这个问题,以便 python 3.8.3 可以 运行 --prefix --editable pip 安装包。
import setuptools
setuptools.setup(
name="mypkg",
version="0.1.0",
entry_points = {
'console_scripts': ['mypkg=mypkg.__main__:main']
},
package_dir={"": "src"},
packages=setuptools.find_packages(where="src"),
python_requires=">=3.6",
)
UPDATE 因此,在进一步研究之后,很明显是带有 python 3.8.3 的 importlib.metadata 模块导致了此问题问题。它适用于较旧的 from pkg_resources import load_entry_point
,但不适用于 from importlib.metadata import distribution
。 我发现如果我将源 mypkg
包路径添加到我的 $PYTHONPATH
,我可以让它工作。大概这是因为它在那里找到了 mypkg.egg-info
目录。但是,如何在可编辑模式下 importlib.metadata 找到 mypkg 元数据而不需要添加原始源目录?
有关此问题的更多详细信息和可行的解决方案,请查看
https://github.com/python/importlib_metadata/issues/364
基本上您需要在您的 --prefix site-packages
目录中创建一个 sitecustomize.py
文件(您也已将其添加到您的 $PYTHONPATH
中)。 sitecustomize.py
自动加载,可用于将路径附加到 sys.path
import os
import io
import sys
try:
names = os.listdir('.')
except OSError:
pass
names = [name for name in names if name.endswith(".pth")]
for name in sorted(names):
try:
f = io.TextIOWrapper(io.open_code(name), encoding="utf-8")
except OSError:
pass
with f:
for n, line in enumerate(f):
if line.startswith("#"):
continue
if line.strip() == "":
continue
try:
if line.startswith(("import ", "import\t")):
exec(line)
continue
line = line.rstrip()
if os.path.exists(line):
sys.path.append(line)
except Exception:
print("Error processing line {:d} of {}:\n".format(n+1, name),
file=sys.stderr)
import traceback
for record in traceback.format_exception(*sys.exc_info()):
for line in record.splitlines():
print(' '+line, file=sys.stderr)
print("\nRemainder of file ignored", file=sys.stderr)
break
Update 从各方面看来 python 3.8-3.10 没有遵循 .egg-link 或 easy-install.pth 存根正确获取 .egg-info 元数据。不知道为什么。尝试使用 brew 安装 python 3.10.1 并且 importlib.metadata 在 .egg-link 或 easy-install.pth 文件之后正确查找 .egg-info 元数据也有问题,尽管 .egg-link 和 easy-install.pth 在 $PYTHONPATH
背景: 我们工作的 CentOS 8 服务器安装了 python 3.6.8(使用 pip 9.0.3) .在工作时
一个项目,我们使用模块实用程序加载特定版本的程序,包括 python 3.8.3 (使用 pip 20.2.2)。在项目目录下是它自己的 bin/、lib/ 等。这允许我们将项目特定的 python 包安装到这些项目目录。其中有一个内部开发的包,我们使用该包在 console_scripts 入口点的帮助下管理我们的项目。这个内部开发的包在 git 的 VCS 下,可以在项目的生命周期内进行编辑。因此,在这个项目的上下文中工作时,我们希望能够编辑这个 python 包的源代码,同时将它安装在本地,以便可以使用它的控制台脚本.这只是 pip install --prefix project_dir -e pkg_src_dir
问题是,这适用于 python 3.6.8,但不适用于 python 3.8.3,这是我们实际用于项目的版本。而且我不确定它是否是 importlib.metadata 特定版本的错误,包括 python 3.8.3.
我创建了一个虚拟的 Hello World 包来尝试调试它。 mypkg.py 定义了一个打印 Hello World 的函数。 main.py 的 main() 函数调用了 mypkg 的 Hello World 打印函数。简单且此结构遵循python.org自己的打包教程。
mypkg/
├── setup.py
└── src/
└── mypkg/
├── __init__.py
├── mypkg.py
└── __main__.py
使用 python 3.6.8 及其 pip 9.0.3,pip install --prefix project_dir -e mypkg
的工作方式与您预期的一样。 project_dir/lib/python-3.6.8/site-packages
包含指向 mypkg/src
目录的 mypkg.egg-link
文件。在 project_dir/bin
中是 mypkg
控制台脚本。
#!/usr/bin/python3.6
# EASY-INSTALL-ENTRY-SCRIPT: 'mypkg','console_scripts','mypkg'
__requires__ = 'mypkg'
import re
import sys
from pkg_resources import load_entry_point
if __name__ == '__main__':
sys.argv[0] = re.sub(r'(-script\.pyw?|\.exe)?$', '', sys.argv[0])
sys.exit(
load_entry_point('mypkg', 'console_scripts', 'mypkg')()
)
通过将 project_dir/lib/python-3.6/site-packages
目录添加到 $PYTHONPATH
之前,我能够 运行 这个控制台脚本 mypkg
没有问题并让它打印 Hello World。我什至可以 运行 这个带有 python 3.8.3 的控制台脚本 运行 直接用 python 的那个版本 python, python-3.8.3 ./mypkg
。这是因为,正如我后来发现的那样,因为它使用的是 pkg_resources 中较旧的 load_entry_point 函数,而不是 importlib.metadata.
但是,如果我尝试以完全相同的方式使用 python 3.8.3 安装相同的软件包,控制台脚本将无法 运行。这是在将 $PYTHONPATH
更新为 project_dir/lib/python-3.8/site-packages
之后。
Traceback (most recent call last):
File "./mypkg", line 33, in <module>
sys.exit(load_entry_point('mypkg', 'console_scripts', 'mypkg')())
File "./mypkg", line 22, in importlib_load_entry_point
for entry_point in distribution(dist_name).entry_points
File "/tools/conda/anaconda3/2020.07/lib/python3.8/importlib/metadata.py", line 504, in distribution
return Distribution.from_name(distribution_name)
File "/tools/conda/anaconda3/2020.07/lib/python3.8/importlib/metadata.py", line 177, in from_name
raise PackageNotFoundError(name)
importlib.metadata.PackageNotFoundError: mypkg
控制台脚本存根有很大不同,这些更改与使用 importlib.metadata 提供 load_entry_point 功能有关。
#!/tools/conda/anaconda3/2020.07/bin/python
# EASY-INSTALL-ENTRY-SCRIPT: 'mypkg','console_scripts','mypkg'
import re
import sys
# for compatibility with easy_install; see #2198
__requires__ = 'mypkg'
try:
from importlib.metadata import distribution
except ImportError:
try:
from importlib_metadata import distribution
except ImportError:
from pkg_resources import load_entry_point
def importlib_load_entry_point(spec, group, name):
dist_name, _, _ = spec.partition('==')
matches = (
entry_point
for entry_point in distribution(dist_name).entry_points
if entry_point.group == group and entry_point.name == name
)
return next(matches).load()
globals().setdefault('load_entry_point', importlib_load_entry_point)
if __name__ == '__main__':
sys.argv[0] = re.sub(r'(-script\.pyw?|\.exe)?$', '', sys.argv[0])
sys.exit(load_entry_point('mypkg', 'console_scripts', 'mypkg')())
有趣的是,当 运行 直接使用 python 3.6.8 二进制文件时,此控制台脚本可以正常工作。 编辑:这是有道理的,因为它回退到加载旧的 pkg_resources 版本的 load_entry_point,因为围绕导入的所有尝试 尽管有两个安装在 sys.path 搜索中共享相同的本地路径(即 project_dir/lib/python-3.8/site-packages
)。只有他们的 system/installation 特定路径不同,其中本地 mypkg
应该找不到。
我还发现,如果我将 python 3.6.8 控制台脚本中的 from pkg_resources import load_entry_point
行添加到 python 3.8.3 控制台脚本,当 运行 使用 python 3.8.3 时,我不再收到错误。 编辑:这又是完全有道理的,因为问题的根源与 importlib.metadata
这是我的 setup.py
的完整披露。我不确定是否可以添加一些东西来解决这个问题,以便 python 3.8.3 可以 运行 --prefix --editable pip 安装包。
import setuptools
setuptools.setup(
name="mypkg",
version="0.1.0",
entry_points = {
'console_scripts': ['mypkg=mypkg.__main__:main']
},
package_dir={"": "src"},
packages=setuptools.find_packages(where="src"),
python_requires=">=3.6",
)
UPDATE 因此,在进一步研究之后,很明显是带有 python 3.8.3 的 importlib.metadata 模块导致了此问题问题。它适用于较旧的 from pkg_resources import load_entry_point
,但不适用于 from importlib.metadata import distribution
。 我发现如果我将源 mypkg
包路径添加到我的 $PYTHONPATH
,我可以让它工作。大概这是因为它在那里找到了 mypkg.egg-info
目录。但是,如何在可编辑模式下 importlib.metadata 找到 mypkg 元数据而不需要添加原始源目录?
有关此问题的更多详细信息和可行的解决方案,请查看
https://github.com/python/importlib_metadata/issues/364
基本上您需要在您的 --prefix site-packages
目录中创建一个 sitecustomize.py
文件(您也已将其添加到您的 $PYTHONPATH
中)。 sitecustomize.py
自动加载,可用于将路径附加到 sys.path
import os
import io
import sys
try:
names = os.listdir('.')
except OSError:
pass
names = [name for name in names if name.endswith(".pth")]
for name in sorted(names):
try:
f = io.TextIOWrapper(io.open_code(name), encoding="utf-8")
except OSError:
pass
with f:
for n, line in enumerate(f):
if line.startswith("#"):
continue
if line.strip() == "":
continue
try:
if line.startswith(("import ", "import\t")):
exec(line)
continue
line = line.rstrip()
if os.path.exists(line):
sys.path.append(line)
except Exception:
print("Error processing line {:d} of {}:\n".format(n+1, name),
file=sys.stderr)
import traceback
for record in traceback.format_exception(*sys.exc_info()):
for line in record.splitlines():
print(' '+line, file=sys.stderr)
print("\nRemainder of file ignored", file=sys.stderr)
break