打包Sage库以供分发

模块、包、分发包

Sage库由大量的Python模块组成,这些模块被组织成一组分层的包来填充命名空间 sage 。所有源文件都位于目录的子目录中 SAGE_ROOT/src/sage/

例如,

  • 该文件 SAGE_ROOT/src/sage/coding/code_bounds.py 提供模块 sage.coding.code_bounds

  • 包含此文件的目录, SAGE_ROOT/src/sage/coding/ ,从而提供了套餐 sage.coding

在Python中还有另一个“包”的概念,即 distribution package (也称为“发行版”或“PIP可安装包”)。目前,整个Sage库由单个发行版提供, sagemath-standard ,它从目录中生成 SAGE_ROOT/pkgs/sagemath-standard

请注意,分发名称不一定要是一个Python标识符。事实上,使用破折号 (- )在分发名称中优先使用下划线; setuptools 而Python打包基础设施的其他部分将下划线标准化为破折号。(在分发名称中使用圆点,以指示组织的所有权,仍在 PEP 423 ,似乎在很大程度上已经失宠,我们不会在SageMath项目中使用它。)

一个发行版,它在 sage.* 命名空间,主要来自 sage.PAC.KAGE ,应该命名为 sagemath-DISTRI-BUTION 。示例:

  • 分布情况 sagemath-categories 提供了Sage库模块的一小部分,主要来自包 sage.structuresage.categories ,以及 sage.misc

其他发行版不应使用前缀 sagemath- 在分发名称中。示例:

  • 分布情况 sage-sws2rst 提供了Python包 sage_sws2rst ,因此它不会填充 sage.* 命名空间,因此不使用前缀 sagemath-

一个发行版,它提供不需要从 sage 命名空间不应使用 sage 它自己的包/模块的命名空间。它应该被定位为一般的Python生态系统的一部分,而不是特定于Sage的发行版。例如:

  • 分布情况 pplpy 提供了Python包 ppl 它是过去的一个大大扩展的版本 sage.libs.ppl ,Sage类库的一部分。套餐 sage.libs.ppl 依赖于 sage.rings 若要转换为Sage Number类型或从Sage Number类型转换,请执行以下操作。 pplpy 没有这样的依赖关系,因此可以在更广泛的Python项目中使用。

  • 分布情况 memory-allocator 提供了Python包 memory_allocator 。这曾经是 sage.ext.memory_allocator ,Sage类库的一部分。

普通包与隐式命名空间包

Sage库的每个模块必须恰好打包在一个分发包中。但是,包中的模块可能包含在不同的分发包中。在这方面,有一个重要的约束条件,即普通程序包(带有 __init__.py 文件)不能拆分成多个分发包。

通过删除 __init__.py 但是,我们可以将该包设置为“隐式”(或“原生”)“命名空间”包,如下所示 PEP 420 。隐式命名空间包可以包含在多个分发包中。因此,每当有两个发行包为模块提供一个公共前缀的Python包时,该前缀需要是隐式名称空间包,即不能有 __init__.py 文件。

例如,

  • sagemath-tdlib 将提供 sage.graphs.graph_decompositions.tdlib

  • sagemath-rw 将提供 sage.graphs.graph_decompositions.rankwidth

  • sagemath-graphs 将提供所有剩余的 sage.graphs.graph_decompositions (以及大部分 sage.graphs )。

那么,没有一个

  • sage

  • sage.graphs

  • sage.graphs.graph_decomposition

可以是普通的包(带有 __init__.py 文件),但它们中的每一个都必须是隐式名称空间包(no __init__.py 文件)。

对于隐式名字空间包, __init__.py 不能再用于初始化包。

在Sage 9.6开发周期中,默认情况下我们仍然使用普通包,但有几个包被转换为隐式命名空间包以支持模块化。

分发包的源目录

Sage库的开发对所有填充 sage.* 命名空间。这意味着这些发行版的源树包含在单个 git 存储库,位于的子目录中 SAGE_ROOT/pkgs

所有这些分发包都有匹配的版本号。从单一发行版的观点来看,这意味着有时会有某个发行版的新发行版,其中唯一变化的是版本号。

分发包的源目录,如 SAGE_ROOT/pkgs/sagemath-standard 包含以下文件:

  • sage -- a relative symbolic link to the monolithic Sage library source tree SAGE_ROOT/src/sage/

  • MANIFEST.in --控制单片Sage库源代码树的哪些文件和目录包含在分发中

    清单应与表单的指令保持同步 # sage_setup: distribution = sagemath-polyhedra 在源文件的顶部。Sage提供了一个工具 sage --fixdistributions 来协助完成这项任务。例如::

    $ ./sage --fixdistributions --set sagemath-polyhedra \
         src/sage/geometry/polyhedron/base*.py
    

    添加或更新指定文件中的指令;以及:

    $ ./sage --fixdistributions --add sagemath-polyhedra \
         src/sage/geometry/polyhedron
    

    将指令添加到给定目录中尚未包含指令的所有文件。

    在构建分发之后(例如,通过命令 make pypi-wheels )或者至少已经构建了sdist(例如,通过命令 make sagemath_polyhedra-sdist ),则可以使用开关更新源分发中所有文件中的分发指令 --from--egg-info **

    $ ./sage --fixdistributions --set sagemath-polyhedra --from-egg-info
    

    要管理所有发行版,请使用::

    $ ./sage --fixdistributions --set all --from-egg-info
    
  • pyproject.toml, setup.cfg, and requirements.txt --标准的Python打包元数据,声明分发名称、依赖关系等。

  • README.rst --对分布的描述

  • LICENSE.txt -- relative symbolic link to the same files in SAGE_ROOT/src

  • VERSION.txt --程序包版本。此文件由发布经理通过运行 sage-update-version 剧本。

    有时可能需要将分发包的修补程序上载到PyPI。应通过添加后缀来标记它们 .post1.post2 ;请参阅 PEP 440 on post-releases 。例如,如果当前开发版本是 9.7.beta8 ,则可以标记这样的版本 9.7.beta8.post1

    此外,有时在PR上工作时,可能需要增加版本,因为另一个分发包中需要新功能。这样的版本应该使用预期的下一个开发版本的版本号并添加后缀来标记 .dev1.dev2 ..。(见 PEP 440 on developmental releases )。例如,如果当前开发版本是 9.7.beta8 ,使用 9.7.beta9.dev1 。如果当前开发版本是稳定版本 9.8 ,使用 9.9.beta0.dev1

    请购单在下一个开发版本中合并后,将再次与其他包版本同步。

  • setup.py --a setuptools 基于安装脚本

  • tox.ini -- configuration for testing with tox

使用符号链接指向 SAGE_ROOT/src 使得模块化的努力能够保持 SAGE_ROOT/src 树整体:模块化一直在幕后发生,不会改变Sage开发人员找到源文件的位置。

其中一些文件实际上可能是从带有后缀的源文件生成的 .m4 由. SAGE_ROOT/bootstrap 脚本通过 m4 宏处理器。

对于每个分发包,还有一个子目录 SAGE_ROOT/build/pkgs/ ,它包含特定于Sage-the-发行版的构建基础结构。请注意,这些子目录遵循不同的命名约定,使用下划线而不是破折号,请参见 目录结构 。因为分发包包含在源代码树中,所以我们将它们设置为“脚本包”而不是“普通包”,请参见 包源类型

依赖项和分发包

在将Sage库的一部分准备为分发包时,依赖关系很重要。

构建时依赖项

如果库的一部分包含任何Cython模块,则在发行包的轮子构建阶段编译这些模块。如果Cython模块使用 cimport 从任何地方拉进任何东西 .pxd 文件,则这些文件必须是正在构建的发行版附带的部分的一部分,或者提供这些文件的发行版必须安装在构建环境中。此外,Cython模块使用的任何C/C++库都必须可以从构建环境访问。

Declaring build-time dependencies: Modern Python packaging provides a mechanism to declare build-time dependencies on other distribution packages via the file pyproject.toml ([build-system] requires); this has superseded the older setup_requires declaration. (There is no mechanism to declare anything regarding the C/C++ libraries.)

而命名空间 sage.* 粗略地根据数学字段或类别进行组织,但我们如何将实现模块划分为分发包必须遵守构建时依赖项施加的硬约束。

We can define some meaningful small distributions that just consist of a single or a few Cython modules. For example, sagemath-tdlib (:issue:`29864`) would just package the single Cython module that must be linked with tdlib, sage.graphs.graph_decompositions.tdlib. Starting with the Sage 9.6 development cycle, as soon as namespace packages are activated, we can start to create these distributions. This is quite a mechanical task.

Reducing build-time dependencies: 有时,可以用运行时依赖项替换库上的Cython模块的构建时依赖项。在其他情况下,可以将同时依赖于多个库的模块拆分成更小的模块,每个模块都有更窄的依赖关系。

模块级别的运行时依赖项

任何 import 在导入模块时,将执行Python或Cython模块顶层的语句。因此,导入的模块必须是发行版的一部分,或者由另一个发行版提供--然后必须声明为运行时依赖项。

Declaring run-time dependencies: 这些依赖项在 setup.cfg (生成自 setup.cfg.m4 )作为 install_requires

Reducing module-level run-time dependencies:

  • 避免从以下位置导入 sage.PAC.KAGE.all 模块时 sage.PAC.KAGE 是一个命名空间包。的主要目的是 *.all 模块用于填充全球交互环境,用户可以在 sage: 提示。特别是,不应该从任何Sage库代码导入 sage.rings.all

    要审核Sage库中的此类导入,请使用 sage --tox -e relint 。在大多数情况下,可以使用该工具自动修复导入 sage --fiximports

  • 用方法级导入替换模块级导入。请注意,这会带来很小的运行时开销,如果在紧密的内部循环中调用该方法,则会很明显。

  • Sage提供了 lazy_import() 机制。可以在模块级别声明延迟导入,但实际的导入仅按需完成。如果导入的模块不存在,则此时会出现运行时错误。当几个方法中需要相同的导入时,这与方法中的本地导入相比要方便一些。

  • 避免从另一个模块导入类只是为了运行 isinstance(object, Class) 测试,特别是在模块实现时 Class 有很大的依赖性。例如,导入类 pAdicField (或函数 is_pAdicField )需要库ntl和pari。

    相反,在只有轻度依赖关系的模块中提供抽象基类(ABC) Class 的一个子类 ABC ,并使用 isinstance(object, ABC) 。例如, sage.rings.abc 为许多环(父)类提供抽象基类,包括 sage.rings.abc.pAdicField 。因此,我们可以替换::

    from sage.rings.padics.generic_nodes import pAdicFieldGeneric  # heavy dependencies
    isinstance(object, pAdicFieldGeneric)
    

    和::

    from sage.rings.padics.generic_nodes import is_pAdicField      # heavy dependencies
    is_pAdicField(object)                                          # deprecated
    

    发信人::

    import sage.rings.abc                                          # no dependencies
    isinstance(object, sage.rings.abc.pAdicField)
    

    请注意,遍历抽象基类只会导致很小的性能损失:

    sage: object = Qp(5)
    
    sage: from sage.rings.padics.generic_nodes import pAdicFieldGeneric
    sage: %timeit isinstance(object, pAdicFieldGeneric)            # fast                           # not tested
    68.7 ns ± 2.29 ns per loop (...)
    
    sage: import sage.rings.abc
    sage: %timeit isinstance(object, sage.rings.abc.pAdicField)    # also fast                      # not tested
    122 ns ± 1.9 ns per loop (...)
    
  • 如果不可能或不希望为其创建抽象基类 isinstance 测试(例如,当类定义在某个外部包中时),需要使用其他解决方案。

    请注意,Python会缓存成功的模块导入,但每次重复不成功的模块导入都会产生成本:

    sage: from sage.schemes.generic.scheme import Scheme
    sage: sZZ = Scheme(ZZ)
    
    sage: def is_Scheme_or_Pluffe(x):
    ....:    if isinstance(x, Scheme):
    ....:        return True
    ....:    try:
    ....:        from xxxx_does_not_exist import Pluffe            # slow on every call
    ....:    except ImportError:
    ....:        return False
    ....:    return isinstance(x, Pluffe)
    
    sage: %timeit is_Scheme_or_Pluffe(sZZ)                         # fast                           # not tested
    111 ns ± 1.15 ns per loop (...)
    
    sage: %timeit is_Scheme_or_Pluffe(ZZ)                          # slow                           # not tested
    143 µs ± 2.58 µs per loop (...)
    

    这个 lazy_import() 机制可以用来简化此模式,方法是 __instancecheck__() 方法,并具有类似的性能特征:

    sage: lazy_import('xxxx_does_not_exist', 'Pluffe')
    
    sage: %timeit isinstance(sZZ, (Scheme, Pluffe))                # fast                           # not tested
    95.2 ns ± 0.636 ns per loop (...)
    
    sage: %timeit isinstance(ZZ, (Scheme, Pluffe))                 # slow                           # not tested
    158 µs ± 654 ns per loop (...)
    

    只导入一次会更快,例如在加载模块时,并缓存失败。我们可以使用以下成语,它利用了这样一个事实 isinstance 接受以下类型的任意嵌套列表和元组:

    sage: try:
    ....:     from xxxx_does_not_exist import Pluffe               # runs once
    ....: except ImportError:
    ....:     # Set to empty tuple of types for isinstance
    ....:     Pluffe = ()
    
    sage: %timeit isinstance(sZZ, (Scheme, Pluffe))                # fast                           # not tested
    95.9 ns ± 1.52 ns per loop (...)
    
    sage: %timeit isinstance(ZZ, (Scheme, Pluffe))                 # fast                           # not tested
    126 ns ± 1.9 ns per loop (...)
    

其他运行时依赖项

如果 import 语句,则在第一次调用该方法时加载导入的模块。因此,即使方法所需的模块不存在,仍可以导入定义该方法的模块。

因此,是否应该声明运行时依赖项就成了一个问题。如果需要该导入的方法提供了核心功能,那么很可能是。但是,如果它只提供可以被认为是“可选功能”的功能,那么很可能不会,在这种情况下,将由用户来安装启用该可选功能的发行版。

例如,让我们考虑设计一个以包为中心的发行版 sage.coding 。首先,让我们看看它是否使用了符号::

(9.5.beta6) $ git grep -E 'sage[.](symbolic|functions|calculus)' src/sage/coding
src/sage/coding/code_bounds.py:        from sage.functions.other import ceil
...
src/sage/coding/grs_code.py:from sage.symbolic.ring import SR
...
src/sage/coding/guruswami_sudan/utils.py:from sage.functions.other import floor

显然,它并没有以非常实质性的方式:

  • 符号函数的导入 ceil()floor() 很可能被人工函数所取代 integer_floor()integer_ceil()

  • 看一看进口的 SR 通过 sage.coding.grs_code ,看起来 SR 用于运行一些符号和,但doctest不显示符号结果,因此很可能可以替换它。

  • 但是请注意,上面对模块名称的文本搜索只是一种启发式搜索。看“熵”的来源,通过 log 从… sage.misc.functional ,出现了对符号的运行时依赖。事实上,出于这个原因,那里的两个文档测试已经被标记为 # needs sage.symbolic

So if packaged as sagemath-coding, now a domain expert would have to decide whether these dependencies on symbolics are strong enough to declare a runtime dependency (install_requires) on sagemath-symbolics. This declaration would mean that any user who installs sagemath-coding (pip install sagemath-coding) would pull in sagemath-symbolics, which has heavy compile-time dependencies (ECL/Maxima/FLINT/Singular/...).

另一种选择是通过以下方式考虑使用符号 sagemath-coding 只是作为提供一些额外功能的东西,只有当用户还安装了 sagemath-symbolics

Declaring optional run-time dependencies: 可以将这样的可选依赖项声明为 extras_require 在……里面 setup.cfg (生成自 setup.cfg.m4 )。这是一种非常有限的机制--尤其是它不会以任何方式影响发行版的构建阶段。它基本上只提供了一种为可以作为附加组件安装的发行版指定昵称的方法。

在我们的示例中,我们可以声明一个 extras_require 这样用户就可以使用 pip install sagemath-coding[symbolics]

仅Doctest依赖项

Doctest通常使用使用Sage库的其他部分提供的功能构建的示例。这种集成测试是Sage的优势之一;但它也会产生额外的依赖关系。

幸运的是,这些依赖项非常温和,我们可以使用相同的机制来处理它们,这种机制与我们在有可选库的情况下设置文档测试的条件相同:使用 # optional - FEATURE 文档测试中的指令。添加这些指令将允许开发人员单独测试发行版,而不需要所有Sage都在场。

Declaring doctest-only dependencies: 这个 extras_require 上面提到的机制也可以用于这一点。

SAGE文档的依赖项

文档将不会模块化。

但是,Sage参考手册的某些部分可能取决于可选程序包提供的功能。参考手册的这些部分应使用Sphinx指令进行条件化 .. ONLY:: ,如中所述 使参考手册的某些部分以可选功能为条件

依赖项的版本约束

依赖项的版本信息来自文件 build/pkgs/*/install-requires.txtbuild/pkgs/*/package-version.txt 。我们使用 m4 宏处理器,用于在生成的文件中插入版本信息 pyproject.tomlsetup.cfgrequirements.txt

分发包的层次结构

实心箭头表示 install_requires 即声明的运行时依赖项。虚线箭头表示 extras_require 即声明的可选运行时依赖项。图中未显示构建依赖项和用于测试的可选依赖项。

  • sage_conf 是一个配置模块。它提供的配置变量设置由 configure 剧本。

  • sagemath-environment 提供到系统和软件环境的连接。它包括 sage.envsage.featuressage.misc.package_dir

  • sagemath-objects 提供了Sage库模块的一个很小的基本子集,特别是 sage.structure ,一小部分 sage.categories ,以及一部分 sage.misc

  • sagemath-categories 提供了基于sagemath对象的Sage库模块的一个小子集。它提供了所有 sage.categories 以及一小部分 sage.rings

  • sagemath-repl 提供IPython内核和Sage准备器 (sage.repl ),Sage博士测试员 (sage.doctest ),以及来自 sage.misc

测试分发包

当然,我们需要工具来测试Sage库各部分的模块化发行版。

  • 模块化Sage库的分发包必须可单独测试!

  • 但我们也希望继续与Sage的其他部分进行集成测试!

为模块化测试准备文档测试

部分 编写可测试的示例 解释如何为Sage编写文档测试。在这里,我们展示了如何准备现有的或新的文档测试,以便它们适合于模块化测试。

每节 影响文档测试的特殊标记 ,每当特定测试需要可选的包时,我们都使用doctest标记 # optional 。该机制还可用于以SAGE库的一部分的存在为条件进行文档测试。

可用的标记采用包或模块名称的形式,例如 sage.combinatsage.graphssage.plotsage.rings.number_fieldsage.rings.real_double ,以及 sage.symbolic 。它们通过以下方式定义 Feature 模块中的子类 sage.features.sagemath ,它还提供从特性到提供它们的发行版的映射(实际上是到SPKG名称的映射)。使用此映射,Sage可以向用户发出安装提示。

例如,包 sage.tensor 是纯代数的,不依赖于符号。然而,有一小部分文档测试依赖于 sage.symbolic.ring.SymbolicRing 用于集成测试。因此,这些文档测试被标记为取决于功能 sage.symbolic

按照惯例,因为 sage.symbolic 出现在Sage的标准安装中,我们使用关键字 # needs 而不是 # optional 。这两个关键字具有相同的语义;工具 sage --fixdoctests 根据约定重写doctest标记。

在定义新特性以使文档测试符合条件时,对特性名称隐藏实现细节可能是个好主意。例如,所有使用大型有限域的文档测试都必须依赖于pari。但是,我们已经定义了一个要素 sage.rings.finite_rings (这意味着存在 sage.libs.pari )。给文档测试打分 # needs sage.rings.finite_rings 使用更清晰的方式表示依赖项 # needs sage.libs.pari ,当实施细节发生变化时,维护负担会更小。

使用TOX测试虚拟环境中的分布

第二章 运行Sage‘s Doctest 详细说明如何使用各种选项运行Sage doctester。

为了测试模块化的Sage库的分发包,我们使用一个虚拟环境,在该环境中我们只安装要测试的分发包(及其Python依赖项)。

让我们首先使用由发行版表示的整个Sage库来尝试它 sagemath-standard 。请注意,在正常构建Sage之后,中提供了一组用于大多数已安装的Python分发包的轮子 SAGE_VENV/var/lib/sage/wheels/ **

$ ls venv/var/lib/sage/wheels
Babel-2.9.1-py2.py3-none-any.whl
Cython-0.29.24-cp39-cp39-macosx_11_0_x86_64.whl
Jinja2-2.11.2-py2.py3-none-any.whl
...
scipy-1.7.2-cp39-cp39-macosx_11_0_x86_64.whl
setuptools-58.2.0-py3-none-any.whl
...
wheel-0.37.0-py2.py3-none-any.whl
widgetsnbextension-3.5.1-py2.py3-none-any.whl
zipp-3.5.0-py3-none-any.whl

但是,在使用默认配置的Sage内部版本中 configure --enable-editable ,将不会有轮子来分发 sage_*sagemath-*

要创建这些控制盘,请使用命令 make wheels **

$ make wheels
...
$ ls venv/var/lib/sage/wheels/sage*
...
sage_conf-10.0b2-py3-none-any.whl
...

(您也可以使用 ./configure --enable-wheels 以确保这些轮子始终可用且最新。)

特别要注意的是, sage-conf ,它提供配置变量设置和到安装在 SAGE_LOCAL

我们现在可以设置一个单独的虚拟环境,在其中安装这些轮子和我们的发行版以进行测试。这就是 tox 发挥作用:它是用于创建用于测试的一次性虚拟环境的标准Python工具。中的每一个分发 SAGE_ROOT/pkgs/ 提供配置文件 tox.ini

遵循文件中的注释 SAGE_ROOT/pkgs/sagemath-standard/tox.ini ,我们可以尝试以下命令:

$ ./bootstrap && ./sage -sh -c '(cd pkgs/sagemath-standard && SAGE_NUM_THREADS=16 tox -v -v -v -e sagepython-sagewheels-nopypi)'

此命令不会对Sage的正常安装进行任何更改。在的子目录中创建虚拟环境 SAGE_ROOT/pkgs/sagemath-standard/.tox/ 。命令完成后,我们可以开始在其虚拟环境中单独安装Sage库:

$ pkgs/sagemath-standard/.tox/sagepython-sagewheels-nopypi/bin/sage

我们还可以运行测试套件的一部分::

$ pkgs/sagemath-standard/.tox/sagepython-sagewheels-nopypi/bin/sage -tp 4 src/sage/graphs/

整体 .tox 可以随时安全地删除目录。

我们可以对其他发行版做同样的事情,例如大型发行版 sagemath-standard-no-symbolics (发件人 :issue:`35095` ),它旨在提供标准Sage库中当前的所有内容,即不依赖于可选的包,但没有包 sage.symbolicsage.calculus

同样,我们可以使用以下命令运行测试 tox 在单独的虚拟环境中:

$ ./bootstrap && make wheels && ./sage -sh -c '(cd pkgs/sagemath-standard-no-symbolics && SAGE_NUM_THREADS=16 tox -v -v -v -e sagepython-sagewheels-nopypi-norequirements)'

一些小的分布,例如提供两个最低水平的分布, sagemath-objectssagemath-categories (发件人 :issue:`29865` ),无需依赖Sage Build的车轮即可进行安装和测试:

$ ./bootstrap && ./sage -sh -c '(cd pkgs/sagemath-objects && SAGE_NUM_THREADS=16 tox -v -v -v -e sagepython)'

此命令查找在PyPI上声明的构建时和运行时依赖项(作为源代码tarball或预构建的轮子),并构建和安装发行版 sagemath-objects 在虚拟环境中,位于 pkgs/sagemath-objects/.tox

构建这些小的发行版是一个有价值的回归测试套件。然而,这两个发行版当前的一个问题是它们不能单独测试:这些模块的文档测试依赖于来自Sage库更高级别部分的许多其他功能。这一问题正在 :issue:`35095`