distutils:如何将用户定义的参数传递给 setup.py?

Posted

技术标签:

【中文标题】distutils:如何将用户定义的参数传递给 setup.py?【英文标题】:distutils: How to pass a user defined parameter to setup.py? 【发布时间】:2010-10-15 05:20:51 【问题描述】:

请提示我如何将用户定义的参数从命令行和 setup.cfg 配置文件传递到 distutils 的 setup.py 脚本。我想编写一个 setup.py 脚本,它接受我的包特定参数。例如:

python setup.py install -foo myfoo

【问题讨论】:

【参考方案1】:

由于 Setuptools/Distuils 的文档非常糟糕,我自己在寻找答案时遇到了问题。但最终我偶然发现了this 的例子。此外,this 类似的问题也很有帮助。基本上,带有选项的自定义命令如下所示:

from distutils.core import setup, Command

class InstallCommand(Command):
    description = "Installs the foo."
    user_options = [
        ('foo=', None, 'Specify the foo to bar.'),
    ]
    def initialize_options(self):
        self.foo = None
    def finalize_options(self):
        assert self.foo in (None, 'myFoo', 'myFoo2'), 'Invalid foo!'
    def run(self):
        install_all_the_things()

setup(
    ...,
    cmdclass=
        'install': InstallCommand,
    
)

【讨论】:

您好,您运行传递 foo=something 的命令行是什么? 这个问题似乎将install 行为更改为新命令。在我看来,OP 想要修改 install 命令,以便它接受一个新参数而不是完全替换它。 这里有一些我不明白的地方。程序如何知道字符串foo=与变量self.foo相关?通过查看更复杂的示例,我发现人们用下划线交换连字符;所以像input-dir= 这样的东西变成self.input_dir。这一切如何运作?我会很感激解释这一点。 这对我很有用,但是你如何指定多个user_options?你如何扩展它? 很多这些问题都过时了,但我发现阅读sdist 的源代码很有启发性。 github.com/python/cpython/blob/master/Lib/distutils/command/… 它回答了上面的许多问题,例如多个参数等。@JakeStevens-Haas,第二个参数用于替代参数形式,因此如果您想使用 -t--token,那么您可以将附加参数指定为 @ 987654334@【参考方案2】:

这是一个非常简单的解决方案,您所要做的就是过滤掉 sys.argv 并在调用 distutils setup(..) 之前自己处理它。 像这样的:

if "--foo" in sys.argv:
    do_foo_stuff()
    sys.argv.remove("--foo")
...
setup(..)

关于如何使用 distutils 执行此操作的文档很糟糕,最终我遇到了这个:the hitchhikers guide to packaging,它使用了sdist 及其user_options。 我发现 extending distutils 参考资料不是特别有用。

虽然这看起来像是使用 distutils 的“正确”方式(至少是我能找到的唯一一种模糊记录的方式)。我在另一个答案中提到的--with--without 开关上找不到任何东西。

这个 distutils 解决方案的问题在于它对我正在寻找的东西来说太复杂了(你可能也是这种情况)。 添加几十行和子类sdist 对我来说是错误的。

【讨论】:

这个解决方案不正确,因为 --foo 可能用于另一个命令:使用“setup.py build_ext --inplace --foo install”,安装不应该认为它得到了--foo。 恐怕子类化命令是向命令添加选项的唯一方法。然而,这并不像通常想象的那么难。 我不知道你为什么不赞成我举一个我想做的例子。我从来没有声称这是一个解决方案,那么为什么说这是不正确的呢?我提供了指向我能找到的关于该主题的唯一文档的指针,并说它“没有通常认为的那么难”并不能帮助我们找到更好的答案。 抱歉,我误读了您的消息,并认为您打算查看 sys.argv,但您确实要求的是等效的。我试图恢复我的反对票,但 SO 没有像往常一样合作:( 如果您有想要控制的动态install_requires,那就太好了【参考方案3】:

是的,现在是 2015 年,在 setuptoolsdistutils 中添加命令和选项的文档仍然很大程度上缺失。

经过几个令人沮丧的小时后,我想出了以下代码,用于向setup.pyinstall 命令添加自定义选项:

from setuptools.command.install import install


class InstallCommand(install):
    user_options = install.user_options + [
        ('custom_option=', None, 'Path to something')
    ]

    def initialize_options(self):
        install.initialize_options(self)
        self.custom_option = None

    def finalize_options(self):
        #print('The custom option for install is ', self.custom_option)
        install.finalize_options(self)

    def run(self):
        global my_custom_option
        my_custom_option = self.custom_option
        install.run(self)  # OR: install.do_egg_install(self)

值得一提的是 install.run() 检查它是否被称为“本机”或已修补:

if not self._called_from_setup(inspect.currentframe()):
    orig.install.run(self)
else:
    self.do_egg_install()

此时您使用setup注册您的命令:

setup(
    cmdclass=
        'install': InstallCommand,
    ,
    :

【讨论】:

你用来传入参数的命令行是什么?我按照您的示例进行了尝试: python install --custom_option=xxx 但它不起作用。错误消息类似于“没有选项 custom_option” 命令中不允许有下划线。将custom_option= 更改为custom-option 时,可以使用--custom-option=bar 作为参数。确切的错误是distutils.errors.DistutilsGetoptError: invalid long option name 'custom_option' (must be letters, numbers, hyphens only 需要注意的是,这个 InstallCommnad 是在 setup() 被调用之后运行的。这意味着,您无法在 setup() 命令之前控制任何内容,例如根据用户参数构建扩展。有人知道该怎么做吗?比如说明一个 cython 扩展是否应该使用 openmp?【参考方案4】:

您不能真正将自定义参数传递给脚本。但是,以下事情是可能的并且可以解决您的问题:

可以使用--with-featurename 启用可选功能,使用--without-featurename 可以禁用标准功能。 [AFAIR 这需要安装工具] 您可以使用环境变量,但这些变量在 Windows 上必须是 set,而在 linux/OS X (FOO=bar python setup.py) 上可以使用前缀。 您可以使用自己的cmd_classes 扩展 distutils,它可以实现新功能。它们也是可链接的,因此您可以使用它来更改脚本中的变量。 (python setup.py foo install) 将在执行install 之前执行foo 命令。

希望能有所帮助。一般来说,我建议提供更多信息,您的额外参数到底应该做什么,也许有更好的解决方案。

【讨论】:

我在 setuptools 中没有遇到任何 --with-featurename 标志。以防万一其他人偶然发现这个......【参考方案5】:

我成功地使用了一种解决方法来使用类似于 totaam 建议的解决方案。我最终从 sys.argv 列表中弹出了我的额外参数:

import sys
from distutils.core import setup
foo = 0
if '--foo' in sys.argv:
    index = sys.argv.index('--foo')
    sys.argv.pop(index)  # Removes the '--foo'
    foo = sys.argv.pop(index)  # Returns the element after the '--foo'
# The foo is now ready to use for the setup
setup(...)

可以添加一些额外的验证以确保输入正确,但我就是这样做的

【讨论】:

这有点骇人听闻,但它有效并且相对容易理解。可以通过利用 argparse 并用来自 argparse 的位置参数替换 sys.argv 来做类似的事情(并使用任何你想要的关键字参数)。这将是一个更大的 hack,但允许人们利用 argparse。 更好的是,剩余的未使用的参数可以作为ArgumentParser.parse_known_arguments返回的第二个值获得。这是首选,因为它还将处理非位置的未使用 args(从而避免关于 setuptools 的假设)。使用argparse 会好很多。此外,用未使用的 args 替换 sys.argv 并不比弹出位置 args 更多。他们都只是简单地删除消耗的参数,以“将剩余的参数传递给另一个脚本或程序”。【参考方案6】:

类似于 totaam 给出的一种快速简便的方法是使用 argparse 获取 -foo 参数并将剩余的参数留给 distutils.setup() 的调用。为此使用 argparse 比手动迭代 sys.argv 更好。例如,在 setup.py 的开头添加:

argparser = argparse.ArgumentParser(add_help=False)
argparser.add_argument('--foo', help='required foo argument', required=True)
args, unknown = argparser.parse_known_args()
sys.argv = [sys.argv[0]] + unknown

add_help=False 参数意味着您仍然可以使用-h 获得常规的 setup.py 帮助(前提是给出了--foo)。

【讨论】:

撤回支持@Cerin 给出的答案 为什么是Retracted in favour of the answer given by @Cerin【参考方案7】:

也许你是一个像我一样没有经验的程序员,在阅读了上面所有的答案后仍然在挣扎。因此,您可能会发现另一个可能有用的示例(并解决先前答案中有关输入命令行参数的 cmets):

class RunClientCommand(Command):
    """
    A command class to runs the client GUI.
    """

    description = "runs client gui"

    # The format is (long option, short option, description).
    user_options = [
        ('socket=', None, 'The socket of the server to connect (e.g. '127.0.0.1:8000')',
    ]

    def initialize_options(self):
        """
        Sets the default value for the server socket.

        The method is responsible for setting default values for
        all the options that the command supports.

        Option dependencies should not be set here.
        """
        self.socket = '127.0.0.1:8000'

    def finalize_options(self):
        """
        Overriding a required abstract method.

        The method is responsible for setting and checking the 
        final values and option dependencies for all the options 
        just before the method run is executed.

        In practice, this is where the values are assigned and verified.
        """
        pass

    def run(self):
        """
        Semantically, runs 'python src/client/view.py SERVER_SOCKET' on the
        command line.
        """
        print(self.socket)
        errno = subprocess.call([sys.executable, 'src/client/view.py ' + self.socket])
        if errno != 0:
            raise SystemExit("Unable to run client GUI!")

setup(
    # Some other omitted details
    cmdclass=
        'runClient': RunClientCommand,
    ,

以上内容经过测试,来自我编写的一些代码。我还包含了一些更详细的文档字符串,以使事情更容易理解。

至于命令行:python setup.py runClient --socket=127.0.0.1:7777。使用 print 语句进行的快速复查表明,run 方法确实选择了正确的参数。

我认为有用的其他资源(更多更多示例):

Custom distutils commands

https://seasonofcode.com/posts/how-to-add-custom-build-steps-and-commands-to-setuppy.html

【讨论】:

【参考方案8】:

要与python setup.py installpip install . 完全兼容,您需要使用环境变量,因为pip 选项--install-option= 存在错误:

    pip --install-option leaks across lines Determine what should be done about --(install|global)-option with Wheels pip not naming abi3 wheels correctly

这是一个不使用--install-option的完整示例:

import os
environment_variable_name = 'MY_ENVIRONMENT_VARIABLE'
environment_variable_value = os.environ.get( environment_variable_name, None )

if environment_variable_value is not None:
    sys.stderr.write( "Using '%s=%s' environment variable!\n" % (
            environment_variable_name, environment_variable_value ) )

setup(
        name = 'packagename',
        version = '1.0.0',
        ...
)

然后,你可以在 Linux 上这样运行它:

MY_ENVIRONMENT_VARIABLE=1 pip install .
MY_ENVIRONMENT_VARIABLE=1 pip install -e .
MY_ENVIRONMENT_VARIABLE=1 python setup.py install
MY_ENVIRONMENT_VARIABLE=1 python setup.py develop

但是,如果您在 Windows 上,请像这样运行它:

set "MY_ENVIRONMENT_VARIABLE=1" && pip install .
set "MY_ENVIRONMENT_VARIABLE=1" && pip install -e .
set "MY_ENVIRONMENT_VARIABLE=1" && python setup.py install
set "MY_ENVIRONMENT_VARIABLE=1" && python setup.py develop

参考资料:

    How to obtain arguments passed to setup.py from pip with '--install-option'? Passing command line arguments to pip install Passing the library path as a command line argument to setup.py

【讨论】:

“搞砸”而不提供确实搞砸的pip 版本对以后看到这个答案的人没有帮助。 它被窃听了,我现在在答案中明确地证明了这一点。此外,这个答案有一个干净的代码,任何人都可以轻松理解并自行尝试。这个答案对于那些正在与无耻的pipsetuptools 错误作斗争的人非常有用。 我的意思不是pip 是否存在问题,而是一旦该问题在未来得到解决 - 比如说 - 这个答案将变得令人难以置信的混乱......那就是为什么你应该限定pip 的哪些版本受到/受到影响。这就是我要说的...... 我明白了。我只是不希望他们能解决任何问题。无论如何,无论 pip 是否存在错误,使用环境变量都是有效的解决方案。我没有发布 pip 版本,但答案有问题的链接,一旦它们被关闭,你就会知道它们是固定的。 这个答案不应该被降级,因为它指出了 pip 和 setuptools 之间的兼容性问题。请注意,pip 20.2 中的 --install-option 将被删除。 github.com/pypa/pip/issues/7309

以上是关于distutils:如何将用户定义的参数传递给 setup.py?的主要内容,如果未能解决你的问题,请参考以下文章

如何将参数传递给用户定义函数?

如何将参数传递给`pymssql`中的`cursor.execute`?

将参数传递给从 R 中的字符串调用的用户定义函数的最佳方法是啥?

如何将参数传递给 Facebook,fb://publish/?

如何将 UDF 的输入参数传递给 sapply

如何将自定义参数传递给事件处理程序