Following system colour scheme - Python 增强提案 Selected dark colour scheme - Python 增强提案 Selected light colour scheme - Python 增强提案

Python 增强提案

PEP 508 – Python 软件包的依赖项规范

作者:
Robert Collins <rbtcollins at hp.com>
BDFL-委托:
Donald Stufft <donald at stufft.io>
讨论列表:
Distutils-SIG 列表
状态:
最终
类型:
标准跟踪
主题:
打包
创建:
2015年11月11日
发布历史:
2015年11月5日,2015年11月16日
决议:
Distutils-SIG 消息

目录

摘要

本 PEP 指定用于描述软件包依赖项的语言。它在描述单个依赖项的边缘划定了一条界限——不同类型的依赖项以及何时应该安装它们是一个更高级别的难题。目的是为更高级别的规范提供构建块。

依赖项的作用是使像 pip [1] 这样的工具能够找到要安装的正确软件包。有时这是非常宽松的——只需指定一个名称,有时则非常具体——引用要安装的特定文件。有时依赖项仅与一个平台相关,或者仅接受某些版本,因此该语言允许描述所有这些情况。

定义的语言是一种紧凑的行格式,已经在 pip 需求文件中得到广泛使用,尽管我们没有指定这些文件允许的命令行选项处理。有一个警告——在 PEP 440 中指定的 URL 引用形式实际上并未在 pip 中实现,但由于 PEP 440 已被接受,因此我们使用该格式而不是 pip 当前的原生格式。

动机

Python 打包生态系统中任何需要使用依赖项列表的规范都需要在此基础上构建一个已批准的 PEP,但 PEP 426 基本上是理想化的——并且已经存在我们反而可以采用的依赖项规范的现有实现。现有的实现经过实战考验且用户友好,因此采用它们可以说比批准一种理想化、未被使用的格式要好得多。

规范

示例

使用基于名称的查找显示的语言的所有功能

requests [security,tests] >= 2.8.1, == 2.8.* ; python_version < "2.7"

最小的基于 URL 的查找

pip @ https://github.com/pypa/pip/archive/1.3.1.zip#sha1=da9234ee9982d4bbb3c72346a6de940a148ea686

概念

依赖项规范始终指定一个分发名称。它可能包含额外功能,这些额外功能扩展了命名分发的依赖项以启用可选功能。可以使用版本限制或提供要安装的特定工件的 URL 来控制安装的版本。最后,可以使用环境标记使依赖项成为条件。

语法

我们首先简要介绍一下语法,然后在后面详细介绍每个部分的语义。

分发规范以 ASCII 文本编写。我们使用 parsley [2] 语法来提供精确的语法。预计该规范将嵌入到一个更大的系统中,该系统提供诸如注释、通过延续支持多行或其他此类功能的框架。

完整的语法(包括用于构建有用解析树的注释)包含在 PEP 的末尾。

可以根据 PEP 440 规则指定版本。(注意:URI 在 std-66 中定义)

version_cmp   = wsp* '<' | '<=' | '!=' | '==' | '>=' | '>' | '~=' | '==='
version       = wsp* ( letterOrDigit | '-' | '_' | '.' | '*' | '+' | '!' )+
version_one   = version_cmp version wsp*
version_many  = version_one (wsp* ',' version_one)*
versionspec   = ( '(' version_many ')' ) | version_many
urlspec       = '@' wsp* <URI_reference>

环境标记允许使规范仅在某些环境中生效

marker_op     = version_cmp | (wsp* 'in') | (wsp* 'not' wsp+ 'in')
python_str_c  = (wsp | letter | digit | '(' | ')' | '.' | '{' | '}' |
                 '-' | '_' | '*' | '#' | ':' | ';' | ',' | '/' | '?' |
                 '[' | ']' | '!' | '~' | '`' | '@' | '$' | '%' | '^' |
                 '&' | '=' | '+' | '|' | '<' | '>' )
dquote        = '"'
squote        = '\\''
python_str    = (squote (python_str_c | dquote)* squote |
                 dquote (python_str_c | squote)* dquote)
env_var       = ('python_version' | 'python_full_version' |
                 'os_name' | 'sys_platform' | 'platform_release' |
                 'platform_system' | 'platform_version' |
                 'platform_machine' | 'platform_python_implementation' |
                 'implementation_name' | 'implementation_version' |
                 'extra' # ONLY when defined by a containing layer
                 )
marker_var    = wsp* (env_var | python_str)
marker_expr   = marker_var marker_op marker_var
              | wsp* '(' marker wsp* ')'
marker_and    = marker_expr wsp* 'and' marker_expr
              | marker_expr
marker_or     = marker_and wsp* 'or' marker_and
                  | marker_and
marker        = marker_or
quoted_marker = ';' wsp* marker

可以使用 extras 字段指定分发的可选组件

identifier_end = letterOrDigit | (('-' | '_' | '.' )* letterOrDigit)
identifier    = letterOrDigit identifier_end*
name          = identifier
extras_list   = identifier (wsp* ',' wsp* identifier)*
extras        = '[' wsp* extras_list? wsp* ']'

为我们提供了一个基于名称的需求规则

name_req      = name wsp* extras? wsp* versionspec? wsp* quoted_marker?

以及一个直接引用规范的规则

url_req       = name wsp* extras? wsp* urlspec wsp+ quoted_marker?

最终得到可以指定依赖项的统一规则。

specification = wsp* ( url_req | name_req ) wsp*

空白字符

非换行空白字符大多是可选的,没有语义含义。唯一的例外是检测 URL 需求的结尾。

名称

Python 分发名称当前在 PEP 345 中定义。名称充当分发的主要标识符。它们存在于所有依赖项规范中,并且足以成为独立的规范。但是,PyPI 对名称施加了严格的限制——它们必须与不区分大小写的正则表达式匹配,否则将不被接受。因此,在本 PEP 中,我们将标识符的可接受值限制为该正则表达式。名称的完整重新定义可能在未来的元数据 PEP 中进行。正则表达式(使用 re.IGNORECASE 运行)是

^([A-Z0-9]|[A-Z0-9][A-Z0-9._-]*[A-Z0-9])$

额外功能

额外功能是分发的可选部分。分发可以根据需要指定任意数量的额外功能,并且每个额外功能都会导致在依赖项规范中使用该额外功能时声明分发的附加依赖项。例如

requests[security]

额外功能将其定义的依赖项与附加到的分发的依赖项联合起来。上面的示例将导致安装 requests 及其自身依赖项,以及 requests 的“security”额外功能中列出的任何依赖项。

如果列出了多个额外功能,则所有依赖项将联合在一起。

版本

有关版本号和版本比较的更多详细信息,请参阅 PEP 440。版本规范限制了可以使用哪个版本的软件包。它们仅适用于按名称查找的分发,而不是通过 URL 查找的分发。版本比较也用于标记功能。版本周围的可选括号是为了与 PEP 345 兼容,但不能生成,只能接受。

环境标记

环境标记允许依赖项规范提供一个规则来描述何时应该使用该依赖项。例如,考虑一个需要 argparse 的软件包。在 Python 2.7 中,argparse 始终存在。在较旧的 Python 版本中,它必须作为依赖项安装。这可以用以下方式表示

argparse;python_version<"2.7"

标记表达式计算结果为 True 或 False。当它计算结果为 False 时,应忽略依赖项规范。

标记语言的灵感来自 Python 本身,选择它是因为它能够在不运行可能成为安全漏洞的任意代码的情况下安全地对其进行评估。标记最初在 PEP 345 中标准化。本 PEP 修复了在 PEP 426 中描述的设计中观察到的一些问题。

标记表达式中的比较由比较运算符进行类型化。不在 中的 运算符执行与 Python 中字符串相同的作用。当定义了 运算符时, 运算符使用 PEP 440 版本比较规则(即当双方都有有效的版本说明符时)。如果没有定义 PEP 440 行为并且该运算符存在于 Python 中,则该运算符将回退到 Python 行为。否则,应引发错误。例如,以下内容将导致错误

"dog" ~= "fred"
python_version ~= "surprise"

用户提供的常量始终使用 '" 引号编码为字符串。请注意,反斜杠转义未定义,但现有实现确实支持它们。它们不包含在本规范中,因为它们增加了复杂性,并且今天没有观察到的需求。同样,我们不定义非 ASCII 字符支持:我们引用的所有运行时变量都应仅限于 ASCII。

标记语法中的变量(例如“os_name”)解析为在 Python 运行时查找的值。除了“extra”之外,所有值在当今的所有 Python 版本中都已定义——如果未定义值,则标记的实现中存在错误。

未知变量必须引发错误,而不是导致计算结果为 True 或 False 的比较。

在给定的 Python 实现上无法计算其值的变量应针对版本计算结果为 0,对于所有其他变量计算结果为空字符串。

“extra”变量是特殊的。它被 wheel 用于向 wheel METADATA 文件中的给定额外功能指示哪些规范适用,但由于 METADATA 文件基于 PEP 426 的草稿版本,因此目前没有对此进行规范。无论如何,在不进行此特殊处理的情况下,与所有其他未知变量一样,“extra”变量应导致错误。

标记 Python 等效项 示例值
os_name os.name posix, java
sys_platform sys.platform linux, linux2, darwin, java1.8.0_51(注意“linux”来自 Python3,“linux2”来自 Python2)
platform_machine platform.machine() x86_64
platform_python_implementation platform.python_implementation() CPython, Jython
platform_release platform.release() 3.14.1-x86_64-linode39, 14.5.0, 1.8.0_51
platform_system platform.system() Linux, Windows, Java
platform_version platform.version() #1 SMP Fri Apr 25 13:07:35 EDT 2014 Java HotSpot(TM) 64-Bit Server VM, 25.51-b03, Oracle Corporation Darwin Kernel Version 14.5.0: Wed Jul 29 02:18:53 PDT 2015; root:xnu-2782.40.9~2/RELEASE_X86_64
python_version '.'.join(platform.python_version_tuple()[:2]) 3.4, 2.7
python_full_version platform.python_version() 3.4.0, 3.5.0b1
implementation_name sys.implementation.name cpython
implementation_version 见下文定义 3.4.0, 3.5.0b1
extra 错误,除非由解释规范的上下文定义。 测试

implementation_version 标记变量派生自 sys.implementation.version

def format_full_version(info):
    version = '{0.major}.{0.minor}.{0.micro}'.format(info)
    kind = info.releaselevel
    if kind != 'final':
        version += kind[0] + str(info.serial)
    return version

if hasattr(sys, 'implementation'):
    implementation_version = format_full_version(sys.implementation.version)
else:
    implementation_version = "0"

向后兼容性

本 PEP 中的大部分内容已经广泛部署,因此不会造成兼容性问题。

但是,在一些方面,本 PEP 与已部署的基础存在差异。

首先,PEP 440 的直接引用实际上尚未在实际环境中部署,但其设计意图是兼容性地添加,并且在将其添加到 pip 或其他使用分发版中现有依赖项元数据的工具时,不存在已知障碍 - 特别是因为它们无论如何都不允许出现在 PyPI 上传的分发版中。

其次,PEP 426 标记在某些方面已经得到了合理的部署,特别是在 wheels 和 pip 中,它们将使用 python_full_version “2.7.10” 进行版本比较的方式有所不同。具体来说,在 426 中,“2.7.10” 小于 “2.7.9”。这种向后不兼容性是故意的。我们还定义了新的运算符 - “~=” 和 “===”,以及新的变量 - platform_releaseplatform_systemimplementation_nameimplementation_version,这些变量在旧的标记实现中不存在。这些变量在这些实现中会引发错误。这两个功能的用户需要判断在生态系统中支持何时变得足够广泛,以至于使用它们不会造成兼容性问题。

第三,PEP 345 要求在版本说明符周围使用括号。为了接受 PEP 345 依赖项规范,接受括号,但不要生成它们。

基本原理

为了推进任何依赖环境标记的新 PEP,我们需要一个规范,其中包含它们现代化的形式。本 PEP 将所有当前未指定的组件整合到一个指定的表单中。

需求说明符是从 setuptools pkg_resources 文档中的 EBNF 中采用的,因为我们希望避免依赖于事实上的标准,而不是 PEP 指定的标准。

完整语法

完整的 parsley 语法

wsp           = ' ' | '\t'
version_cmp   = wsp* <'<=' | '<' | '!=' | '==' | '>=' | '>' | '~=' | '==='>
version       = wsp* <( letterOrDigit | '-' | '_' | '.' | '*' | '+' | '!' )+>
version_one   = version_cmp:op version:v wsp* -> (op, v)
version_many  = version_one:v1 (wsp* ',' version_one)*:v2 -> [v1] + v2
versionspec   = ('(' version_many:v ')' ->v) | version_many
urlspec       = '@' wsp* <URI_reference>
marker_op     = version_cmp | (wsp* 'in') | (wsp* 'not' wsp+ 'in')
python_str_c  = (wsp | letter | digit | '(' | ')' | '.' | '{' | '}' |
                 '-' | '_' | '*' | '#' | ':' | ';' | ',' | '/' | '?' |
                 '[' | ']' | '!' | '~' | '`' | '@' | '$' | '%' | '^' |
                 '&' | '=' | '+' | '|' | '<' | '>' )
dquote        = '"'
squote        = '\\''
python_str    = (squote <(python_str_c | dquote)*>:s squote |
                 dquote <(python_str_c | squote)*>:s dquote) -> s
env_var       = ('python_version' | 'python_full_version' |
                 'os_name' | 'sys_platform' | 'platform_release' |
                 'platform_system' | 'platform_version' |
                 'platform_machine' | 'platform_python_implementation' |
                 'implementation_name' | 'implementation_version' |
                 'extra' # ONLY when defined by a containing layer
                 ):varname -> lookup(varname)
marker_var    = wsp* (env_var | python_str)
marker_expr   = marker_var:l marker_op:o marker_var:r -> (o, l, r)
              | wsp* '(' marker:m wsp* ')' -> m
marker_and    = marker_expr:l wsp* 'and' marker_expr:r -> ('and', l, r)
              | marker_expr:m -> m
marker_or     = marker_and:l wsp* 'or' marker_and:r -> ('or', l, r)
                  | marker_and:m -> m
marker        = marker_or
quoted_marker = ';' wsp* marker
identifier_end = letterOrDigit | (('-' | '_' | '.' )* letterOrDigit)
identifier    = < letterOrDigit identifier_end* >
name          = identifier
extras_list   = identifier:i (wsp* ',' wsp* identifier)*:ids -> [i] + ids
extras        = '[' wsp* extras_list?:e wsp* ']' -> e
name_req      = (name:n wsp* extras?:e wsp* versionspec?:v wsp* quoted_marker?:m
                 -> (n, e or [], v or [], m))
url_req       = (name:n wsp* extras?:e wsp* urlspec:v (wsp+ | end) quoted_marker?:m
                 -> (n, e or [], v, m))
specification = wsp* ( url_req | name_req ):s wsp* -> s
# The result is a tuple - name, list-of-extras,
# list-of-version-constraints-or-a-url, marker-ast or None


URI_reference = <URI | relative_ref>
URI           = scheme ':' hier_part ('?' query )? ( '#' fragment)?
hier_part     = ('//' authority path_abempty) | path_absolute | path_rootless | path_empty
absolute_URI  = scheme ':' hier_part ( '?' query )?
relative_ref  = relative_part ( '?' query )? ( '#' fragment )?
relative_part = '//' authority path_abempty | path_absolute | path_noscheme | path_empty
scheme        = letter ( letter | digit | '+' | '-' | '.')*
authority     = ( userinfo '@' )? host ( ':' port )?
userinfo      = ( unreserved | pct_encoded | sub_delims | ':')*
host          = IP_literal | IPv4address | reg_name
port          = digit*
IP_literal    = '[' ( IPv6address | IPvFuture) ']'
IPvFuture     = 'v' hexdig+ '.' ( unreserved | sub_delims | ':')+
IPv6address   = (
                  ( h16 ':'){6} ls32
                  | '::' ( h16 ':'){5} ls32
                  | ( h16 )?  '::' ( h16 ':'){4} ls32
                  | ( ( h16 ':')? h16 )? '::' ( h16 ':'){3} ls32
                  | ( ( h16 ':'){0,2} h16 )? '::' ( h16 ':'){2} ls32
                  | ( ( h16 ':'){0,3} h16 )? '::' h16 ':' ls32
                  | ( ( h16 ':'){0,4} h16 )? '::' ls32
                  | ( ( h16 ':'){0,5} h16 )? '::' h16
                  | ( ( h16 ':'){0,6} h16 )? '::' )
h16           = hexdig{1,4}
ls32          = ( h16 ':' h16) | IPv4address
IPv4address   = dec_octet '.' dec_octet '.' dec_octet '.' dec_octet
nz            = ~'0' digit
dec_octet     = (
                  digit # 0-9
                  | nz digit # 10-99
                  | '1' digit{2} # 100-199
                  | '2' ('0' | '1' | '2' | '3' | '4') digit # 200-249
                  | '25' ('0' | '1' | '2' | '3' | '4' | '5') )# %250-255
reg_name = ( unreserved | pct_encoded | sub_delims)*
path = (
        path_abempty # begins with '/' or is empty
        | path_absolute # begins with '/' but not '//'
        | path_noscheme # begins with a non-colon segment
        | path_rootless # begins with a segment
        | path_empty ) # zero characters
path_abempty  = ( '/' segment)*
path_absolute = '/' ( segment_nz ( '/' segment)* )?
path_noscheme = segment_nz_nc ( '/' segment)*
path_rootless = segment_nz ( '/' segment)*
path_empty    = pchar{0}
segment       = pchar*
segment_nz    = pchar+
segment_nz_nc = ( unreserved | pct_encoded | sub_delims | '@')+
                # non-zero-length segment without any colon ':'
pchar         = unreserved | pct_encoded | sub_delims | ':' | '@'
query         = ( pchar | '/' | '?')*
fragment      = ( pchar | '/' | '?')*
pct_encoded   = '%' hexdig
unreserved    = letter | digit | '-' | '.' | '_' | '~'
reserved      = gen_delims | sub_delims
gen_delims    = ':' | '/' | '?' | '#' | '(' | ')?' | '@'
sub_delims    = '!' | '$' | '&' | '\\'' | '(' | ')' | '*' | '+' | ',' | ';' | '='
hexdig        = digit | 'a' | 'A' | 'b' | 'B' | 'c' | 'C' | 'd' | 'D' | 'e' | 'E' | 'f' | 'F'

一个测试程序 - 如果语法在字符串 grammar

import os
import sys
import platform

from parsley import makeGrammar

grammar = """
    wsp ...
    """
tests = [
    "A",
    "A.B-C_D",
    "aa",
    "name",
    "name<=1",
    "name>=3",
    "name>=3,<2",
    "name@http://foo.com",
    "name [fred,bar] @ http://foo.com ; python_version=='2.7'",
    "name[quux, strange];python_version<'2.7' and platform_version=='2'",
    "name; os_name=='a' or os_name=='b'",
    # Should parse as (a and b) or c
    "name; os_name=='a' and os_name=='b' or os_name=='c'",
    # Overriding precedence -> a and (b or c)
    "name; os_name=='a' and (os_name=='b' or os_name=='c')",
    # should parse as a or (b and c)
    "name; os_name=='a' or os_name=='b' and os_name=='c'",
    # Overriding precedence -> (a or b) and c
    "name; (os_name=='a' or os_name=='b') and os_name=='c'",
    ]

def format_full_version(info):
    version = '{0.major}.{0.minor}.{0.micro}'.format(info)
    kind = info.releaselevel
    if kind != 'final':
        version += kind[0] + str(info.serial)
    return version

if hasattr(sys, 'implementation'):
    implementation_version = format_full_version(sys.implementation.version)
    implementation_name = sys.implementation.name
else:
    implementation_version = '0'
    implementation_name = ''
bindings = {
    'implementation_name': implementation_name,
    'implementation_version': implementation_version,
    'os_name': os.name,
    'platform_machine': platform.machine(),
    'platform_python_implementation': platform.python_implementation(),
    'platform_release': platform.release(),
    'platform_system': platform.system(),
    'platform_version': platform.version(),
    'python_full_version': platform.python_version(),
    'python_version': '.'.join(platform.python_version_tuple()[:2]),
    'sys_platform': sys.platform,
}

compiled = makeGrammar(grammar, {'lookup': bindings.__getitem__})
for test in tests:
    parsed = compiled(test).specification()
    print("%s -> %s" % (test, parsed))

PEP 508 的更改摘要

根据其初始实现后的反馈,对本 PEP 做出了以下更改

  • python_version 的定义已从 platform.python_version()[:3] 更改为 '.'.join(platform.python_version_tuple()[:2]),以适应未来可能具有两位数主版本和次版本(例如 3.10)的 Python 版本。[3]

参考文献


来源:https://github.com/python/peps/blob/main/peps/pep-0508.rst

上次修改时间:2023-09-09 17:39:29 GMT