Python requires¶
介绍¶
python_requires 功能是共享不同 recipe 之间文件和代码的一种非常方便的方式。Python require 是一种特殊的 recipe,它不创建包,仅用于被其他 recipes 复用。
一个我们想复用的非常简单的 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" 来要求多个 python-requires。
扩展基类¶
一个常见的用例是声明一个基类,其中包含我们想通过继承在多个 recipes 中复用的方法。我们会将这个基类写在一个 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是 intended for inheritance 的那个类,它不扩展ConanFile。
PyReq是定义当前正在导出的包的那个类,它是 referencepyreq/0.1的 recipe。
一旦我们想要复用的包含基类的包可用,我们就可以在其他 recipes 中使用它来继承该基类的功能。我们需要像之前一样声明 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,以及一个名为 myfile.txt 的文件,其中包含“Hello”文本。
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 来测试 python_require,只需添加一个 test_package/conanfile.py。
from conan import ConanFile
def mynumber():
return 42
class PyReq(ConanFile):
name = "pyreq"
version = "1.0"
package_type = "python-require"
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 的 **补丁** 版本或 **修订** 的更改不会影响包 ID。因此,依赖于
"pyreq/1.2.3"或"pyreq/1.2.4"将导致相同的包 ID(在哈希计算中都映射到"pyreq/1.2.Z")。如果您想更改通用代码但又不想影响消费者或触发被依赖项的重新构建,请升级补丁版本。对 **次要** 版本的更改将产生不同的包 ID。因此,如果您依赖于
"pyreq/1.2.3",然后将版本升级到"pyreq/1.3.0",那么您将需要构建使用该新 python-require 的新二进制文件。如果您想确保需要此 python-require 的包使用这些代码更改进行构建,请升级次要或主要版本。
在大多数情况下,使用版本范围 python_requires = "pyreq/[>=1.0 <2.0]" 是正确的方法,因为这意味着 **主** 版本升级不包含在内,因为它们将需要消费者自身的更改。然后,可以发布新主版本 pyreq/2.0,让消费者逐渐将其要求更改为 python_requires = "pyreq/[>=2.0 <3.0]",修复 recipes,然后继续前进,而不会破坏整个项目。
与常规 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 recipes 只会被解释器加载一次,并且对所有消费者都是通用的。请不要在
python_requiresrecipes 中使用任何全局状态。Python requires 对消费者是私有的,它们不是传递的。不同的消费者可以要求相同
python-require的不同版本。由于是私有的,它们不能从下游以任何方式覆盖。python_requires本身不能使用常规的requires或tool_requires。具有requirements()(及类似方法)供 recipes 继承是可能的并且允许的,但python_requires类本身不能使用它们。python_requires不能被“别名”。python_requires可以使用原生的 Pythonimport来导入其他 Python 文件,只要这些文件与 recipe 一起导出。python_requires也可以作为可编辑包使用。python_requires会被锁定在 lockfiles 中,以像其他requires和tool_requires一样保证可重现性。
注意
最佳实践
即使
python-requires可以传递性地python_requires其他python-requiresrecipes,这是不鼓励的。多层继承和复用可能会变得非常复杂且难以管理,建议保持层级扁平。不要尝试将 Python 继承与
python_requires_extend继承机制混合,它们不兼容并且可能导致问题。请不要为
python-requires使用多重继承。