如何获得setuputils包中入口脚本文件的位置?

15 投票
3 回答
6701 浏览
提问于 2025-04-18 15:32

我在我的setup.py文件的[console_scripts]部分定义了一个入口点。这个命令安装得很好,运行也没问题,但我需要一种方法来程序化地找到这个脚本的路径(比如在Windows上,它的路径可能是C:/my/virtual/env/scripts/my_console_script.exe)。我需要这个路径是为了能把它作为参数传递给其他命令,不管这个包安装在哪里。虽然setuptools提供了pkg_resources,但似乎没有办法直接获取实际安装的路径,只能获取可加载的对象。

编辑:为了让使用场景更清楚,这里是我的设置。

我有一个插件驱动的应用程序,它与各种本地服务进行通信。其中一个插件与一个网络管理系统(NMS)包的警报接口连接。这个警报包唯一能将警报发送到任意处理程序的方法就是调用一个脚本——执行的路径(在这种情况下是console_scripts的入口点)被注册为一个完整的路径——这就是我需要获取的路径。

3 个回答

2

你可以通过以下方式找到site_packages目录的路径:

from distutils.sysconfig import get_python_lib
get_python_lib()

如果你的脚本在这个路径的某个地方,比如在site-packages里或者在上一级目录,你可以把相对路径和site-packages的路径结合起来,像这样:

import os
scriptPath = os.path.realpath(os.path.join(get_python_lib(), '../my/relative/path/script.exe'))

还有一个小技巧是,你可以导入相关的模块,然后查看它的file参数。如果你知道那个文件的相对路径,你就可以像上面的例子那样查找你的脚本。可以在命令行界面试试这个方法。

import django
django.__file__

/usr/lib/python2.6/site-packages/django/init.pyc

modulePath = os.path.split(django.__file__)[0]
scriptPath = os.path.realpath(os.path.join(modulePath, '../my/relative/path/script.exe'))

以上的建议方向对吗?

4

你有没有在你的入口脚本中试过用 `os.path.abspath(__file__)` 呢?这个方法会返回你入口文件的绝对路径。

或者你可以从 distutils.spawn 中调用 find_executable 方法:

import distutils.spawn distutils.spawn.find_executable('executable')

10

你可以给 pip 传一个选项,比如 --install-option="--install-scripts=/usr/local/bin",这样就可以自己设置路径了。不过我理解你可能不想在跨平台项目中硬编码这个路径。

所以,我们需要找出 setuptools 实际使用的目录。可惜的是,这个路径是在一堆实际的设置代码中确定的,而不是在一个我们可以直接调用的单独函数里。

接下来的步骤是写一个自定义的安装命令,观察并保存这个路径。(说到这,你也可以在这个自定义安装类中把 self.install_scripts 设置为你选择的目录,这样就能把这部分配置放在一个地方(包里),而不是分散在包里和设置命令行参数中……)

示例:

from setuptools import setup
from setuptools.command.install import install
from distutils.command.build_py import build_py
import os

class InstallReadsScriptDir(install):
    def run(self):
        self.distribution._x_script_dir = self.install_scripts
        install.run(self)

class BuildConfiguresScriptDir(build_py):
    def run(self):
        build_py.run(self)
        if self.dry_run:
            return
        script_dir = self.distribution._x_script_dir  # todo: check exists
        target = os.path.join(self.build_lib, 'mypackage', 'scriptdir.py')
        with open(target, 'w') as outfile:
            outfile.write('path = """{}"""'.format(script_dir))

setup(
    name="MyPackage",
    version='0.0.1',
    packages = ['mypackage'],
    entry_points = {
        'console_scripts': [
            'footest = mypackage:main',
        ],
    },
    cmdclass= {
        'install': InstallReadsScriptDir,
        'build_py': BuildConfiguresScriptDir,
    },
)

可能的反对意见:

  • 有些人不喜欢任何形式的代码生成。不过在这种情况下,更像是配置,把它放在包里的 .py 文件中可以方便从任何 Python 代码中访问。

  • 乍一看,这似乎有点像是个小窍门。不过,setuptools 是专门设计来允许自定义命令类的。这里没有真正访问任何私有内容,除了可能使用 distribution 对象传值,这只是为了避免使用 global

  • 这个方法只获取目录,而不是可执行文件的名称。不过你应该知道这些名称,因为它们是根据你的入口点配置来的。获取名称需要调用解析方法并生成更多代码。

  • 如果你在没有安装的情况下构建,可能需要一些修复。不过 sdist 是安全的。

  • develop 是可能的,但需要另一个命令类。install 不会被调用,build_py 只有在你设置了 use_2to3 时才会被调用。自定义的 develop 命令可以像 install 示例那样获取路径,但作为 self.script_dir。从那里你需要决定是否愿意在你的源目录中写入文件,这个目录在 self.egg_path 中——因为它会在后续的 install 中被使用,尽管它应该是同一个文件(而且构建命令会覆盖这个文件)。

附录

这个小小的灵感可能更优雅,因为它不需要把路径保存到任何地方,但仍然能获取到 setuptools 使用的实际路径,前提是安装后没有任何配置改变。

from setuptools import Distribution
from setuptools.command.install import install

class OnlyGetScriptPath(install):
    def run(self):
        # does not call install.run() by design
        self.distribution.install_scripts = self.install_scripts

def get_setuptools_script_dir():
    dist = Distribution({'cmdclass': {'install': OnlyGetScriptPath}})
    dist.dry_run = True  # not sure if necessary, but to be safe
    dist.parse_config_files()
    command = dist.get_command_obj('install')
    command.ensure_finalized()
    command.run()
    return dist.install_scripts

附录 2

我刚意识到/记起 pip 会在 egg-info 中创建一个 installed-files.txt,这个文件是一个相对于包根目录的所有文件(包括脚本)的路径列表。使用 pkg_resources.get_distribution('mypackage').get_metadata('installed-files.txt') 可以获取这个文件。不过这只是 pip 的功能,不是由 setup.py install 创建的。你需要逐行查看,寻找你的脚本名称,然后根据你的包目录获取绝对路径。

撰写回答