Python requires

简介

python_requires 功能是一种非常方便的方式,可以在不同的 recipe 之间共享文件和代码。“python require” 是一种特殊的 recipe,它不创建包,仅用于被其他 recipe 重用。

我们想要重用的一个非常简单的 recipe 可能是

from conan import ConanFile

myvar = 123

def myfunct():
    return 234

class Pkg(ConanFile):
    name = "pyreq"
    version = "0.1"
    package_type = "python-require"

然后我们将使用 conan create . 使其可用于其他包。请注意,python-require 包不创建二进制文件,它只是 recipe 部分。

$ conan create .
# It will only export the recipe, but will NOT create binaries
# python-requires do NOT have binaries

我们可以通过在 python_requires 属性中声明依赖关系来重用上述 recipe 功能,并且可以使用 self.python_requires["<name>"].module 访问其成员

from conan import ConanFile

class Pkg(ConanFile):
    name = "pkg"
    version = "0.1"
    python_requires = "pyreq/0.1"

    def build(self):
        v = self.python_requires["pyreq"].module.myvar  # v will be 123
        f = self.python_requires["pyreq"].module.myfunct()  # f will be 234
        self.output.info(f"{v}, {f}")
$ conan create .
...
pkg/0.1: 123, 234

Python requires 也可以使用版本范围,如果这些 python-requires 需要随时间演变,则在许多情况下建议使用版本范围

from conan import ConanFile

class Pkg(ConanFile):
    python_requires = "pyreq/[>=1.0 <2]"

也可以使用 python_requires = "pyreq/0.1", "other/1.2" 来 require 多个 python-requires

扩展基类

一个常见的用例是声明一个基类,其中包含我们想要通过继承在多个 recipe 中重用的方法。我们将在 python-requires 包中编写这个基类

from conan import ConanFile

class MyBase:
    def source(self):
        self.output.info("My cool source!")
    def build(self):
        self.output.info("My cool build!")
    def package(self):
        self.output.info("My cool package!")
    def package_info(self):
        self.output.info("My cool package_info!")

class PyReq(ConanFile):
    name = "pyreq"
    version = "0.1"
    package_type = "python-require"

并使其可通过以下方式重用

$ conan create .

请注意,recipe 文件中有两个类

  • MyBase 是用于继承的类,并且不扩展 ConanFile

  • PyReq 是定义当前正在导出的包的类,它是引用 pyreq/0.1 的 recipe。

一旦包含我们想要重用的基类的包可用,我们就可以在其他 recipe 中使用它来继承该基类的功能。我们需要像之前一样声明 python_requires,并且我们需要在属性 python_requires_extend 中告知 Conan 要使用的基类。在这里,我们的 recipe 将从类 MyBase 继承

from conan import ConanFile

class Pkg(ConanFile):
    name = "pkg"
    version = "0.1"
    python_requires = "pyreq/0.1"
    python_requires_extend = "pyreq.MyBase"

结果继承等效于将我们的 Pkg 类声明为 class Pkg(pyreq.MyBase, ConanFile)。因此,创建包后,我们可以看到如何重用基类中的方法

$ conan create .
...
pkg/0.1: My cool source!
pkg/0.1: My cool build!
pkg/0.1: My cool package!
pkg/0.1: My cool package_info!
...

通常,基类属性不会被继承,应尽可能避免使用。对于某些属性,有方法替代方案,例如 export()set_version()。对于特殊情况,请参阅 init() 方法文档,以获取有关扩展继承属性的更多信息。

可以重新实现一些基类方法,也可以使用 Python super() 语法显式调用基类方法

from conan import ConanFile

class Pkg(ConanFile):
    name = "pkg"
    version = "0.1"
    python_requires = "pyreq/0.1"
    python_requires_extend = "pyreq.MyBase"

    def source(self):
        super().source()  # call the base class method
        self.output.info("MY OWN SOURCE") # Your own implementation

并非必须调用基类方法,完全覆盖而不调用 super() 是可能的。也可以更改调用顺序,并且可以先调用您自己的代码,然后再调用 super()

重用文件

可以访问由使用 python_requires 的 recipe 导出的文件。我们可以有这个 recipe,以及一个包含 “Hello” 文本的 myfile.txt 文件。

from conan import ConanFile

class PyReq(ConanFile):
    name = "pyreq"
    version = "1.0"
    package_type = "python-require"
    exports = "*"
$ echo "Hello" > myfile.txt
$ conan create .

现在 python-require 已创建,我们可以使用 path 属性访问其路径(myfile.txt 所在的路径)

import os

from conan import ConanFile
from conan.tools.files import load

class Pkg(ConanFile):
    python_requires = "pyreq/0.1"

    def build(self):
        pyreq_path = self.python_requires["pyreq"].path
        myfile_path = os.path.join(pyreq_path, "myfile.txt")
        content = load(self, myfile_path)  # content = "Hello"
        self.output.info(content)
        # we could also copy the file, instead of reading it

请注意,只有 exports 适用于这种情况,但 exports_sources 不适用。

测试 python-requires

可以通过添加 test_package/conanfile.py 来使用 test_package 测试 python_require

conanfile.py
from conan import ConanFile

def mynumber():
    return 42

class PyReq(ConanFile):
    name = "pyreq"
    version = "1.0"
    package_type = "python-require"
test_package/conanfile.py
from conan import ConanFile

class Tool(ConanFile):

    # Literal "tested_reference_str", Conan will dynamically replace it
    python_requires = "tested_reference_str"

    def test(self):
        pyreq = self.python_requires["pyreq"].module
        mynumber = pyreq.mynumber()
        self.output.info("{}!!!".format(mynumber))

从 Conan 2.1 开始,python_requires = "tested_reference_str" 是强制性的。在没有此声明的情况下自动注入 python_requires 已被弃用,并且将在未来版本中删除。

请注意,test_package/conanfile.py 不需要任何类型的 python_requires 声明,这是自动且隐式完成的。我们现在可以使用以下命令创建和测试它

$ conan create .
...
pyreq/0.1 (test package): 42!!!

对 package_id 的影响

python_requires 将影响使用这些依赖项的**使用者包**的 package_id。默认情况下,策略为 minor_mode,这意味着

  • 对 python-require 的**修订版本**的 **patch** 版本的更改不会影响包 ID。因此,依赖于 "pyreq/1.2.3""pyreq/1.2.4" 将导致相同的包 ID(两者都将在哈希计算中映射到 "pyreq/1.2.Z")。如果您想更改通用代码,但不希望使用者受到影响或触发依赖项的重建,请增加 patch 版本。

  • 对 **minor** 版本的更改将产生不同的包 ID。因此,如果您依赖于 "pyreq/1.2.3",并且您将版本提升到 "pyreq/1.3.0",那么您将需要构建使用该新 python-require 的新二进制文件。如果您想确保需要此 python-require 的包将使用代码中的这些更改进行构建,请增加 minor 或 major 版本。

在大多数情况下,使用版本范围 python_requires = "pyreq/[>=1.0 <2.0]" 是正确的方法,因为这意味着不包括 **major** 版本提升,因为它们将需要在使用者本身中进行更改。然后可以发布 pyreq/2.0 的新 major 版本,并让使用者逐步更改其 requirements 为 python_requires = "pyreq/[>=2.0 <3.0]",修复 recipe,并在不破坏整个项目的情况下向前发展。

与常规 requires 一样,此默认值可以使用 core.package_id:default_python_mode 配置进行自定义。

也可以使用 package_id() 方法自定义每个包的 python_requires 的效果

from conan import ConanFile

class Pkg(ConanFile):
    python_requires ="pyreq/[>=1.0]"
    def package_id(self):
        self.info.python_requires.patch_mode()

python_requires 的解析

使用 python_requires 时,应考虑一些重要事项

  • Python requires recipe 只由解释器加载一次,并且它们对所有使用者都是通用的。不要在 python_requires recipe 中使用任何全局状态。

  • Python requires 对使用者是私有的。它们不是传递性的。不同的使用者可以 require 相同 python-require 的不同版本。由于是私有的,因此无法从下游以任何方式覆盖它们。

  • python_requires 本身不能使用常规 requirestool_requires。拥有 requirements()(和类似的)方法以供 recipe 继承是可能的并且允许的,但是 python_requires 类本身不能使用它们。

  • python_requires 不能被“别名化”。

  • python_requires 可以使用原生 python import 到其他 python 文件,只要这些文件与 recipe 一起导出即可。

  • python_requires 也可以用作可编辑的包。

  • python_requires 被锁定在 lockfile 中,以保证可再现性,就像其他 requirestool_requires 被锁定的方式一样。

注意

最佳实践

  • 即使 python-requires 可以传递性地 python_requires 其他 python-requires recipe,也不建议这样做。多层继承和重用可能会变得非常复杂且难以管理,建议保持层次结构扁平。

  • 不要尝试将 Python 继承与 python_requires_extend 继承机制混合使用,它们是不兼容的并且可能会破坏。

  • 不要为 python-requires 使用多重继承