依赖说明符

本文档描述了最初在 PEP 508 中指定的依赖说明符格式。

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

所定义的语言是一种紧凑的基于行的格式,已广泛用于 pip 需求文件,尽管我们没有指定这些文件允许的命令行选项处理。有一个警告——在 版本说明符规范 中指定的 URL 引用形式实际上并未在 pip 中实现,但我们使用该格式而不是 pip 当前的原生格式。

规范

示例

语言的所有功能都显示为基于名称的查找

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

概念

依赖规范总是指定一个分发名称。它可以包含 extras,这些 extras 扩展了命名分发的依赖以启用可选功能。安装的版本可以通过版本限制或提供要安装的特定 artifact 的 URL 来控制。最后,依赖可以通过环境标记使其具有条件性。

语法

我们首先简要介绍语法,然后深入研究每个部分的语义。

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

完整的语法,包括用于构建有用解析树的注释,包含在本文档的末尾。

版本可以根据 版本说明符规范 的规则指定。(注意:URI 在 std-66 中定义)

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

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

marker_op     = version_cmp | (wsp+ 'in' wsp+) | (wsp+ 'not' wsp+ 'in' wsp+)
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' | 'extras' | 'dependency_groups' # 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* ']'

extras 的名称限制在 PEP 685 中定义。

给出基于名称的需求的规则

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 中可能会对名称进行完全重新定义。该正则表达式(使用 re.IGNORECASE 运行)是

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

Extras

extra 是分发的可选部分。分发可以指定任意数量的 extras,并且当在依赖规范中使用 extra 时,每个 extra 都会导致声明分发的附加依赖。例如

requests[security,tests]

Extras 在它们定义的依赖与它们所附带的分发的依赖中是并集的。上面的例子将导致安装 requests,以及 requests 自己的依赖,以及 requests 的“security”extra 中列出的任何依赖。

如果列出了多个 extra,所有依赖都将合并在一起。

版本

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

环境标记

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

argparse;python_version<"2.7"

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

标记语言受到 Python 本身的启发,选择它是因为能够安全地评估它而无需运行可能成为安全漏洞的任意代码。标记最初在 PEP 345 中标准化。本文档修复了在 PEP 426 中描述的设计中观察到的一些问题。

标记表达式中的比较根据比较运算符和标记值的类型进行类型化。不在 <version_cmp> 中的 <marker_op> 运算符的行为与 Python 中字符串或集合的行为相同,这取决于标记值本身是字符串还是集合。<version_cmp> 运算符使用 版本说明符规范 的版本比较规则(即当两边都有有效的版本说明符时)。如果此规范没有定义的行为且运算符存在于 Python 中,则运算符将回退到 Python 中涉及类型的行为。否则应引发错误。例如,以下情况将导致错误

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

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

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

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

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

“extra”变量很特殊。它由 wheels 用于指示在 wheel METADATA 文件中哪些规范适用于给定 extra,但由于 METADATA 文件基于 PEP 426 的草案版本,目前没有对此的规范。无论如何,在这种特殊处理发生之外的上下文中,“extra”变量应像所有其他未知变量一样导致错误。

“extras”和“dependency_groups”变量也很特殊。它们用于在从锁定文件安装时指定任何请求的 extras 或依赖组。在锁定文件上下文之外,这两个变量应像所有其他未知变量一样导致错误。

标记

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

除解释规范的上下文定义外,为错误。

字符串

toml

extras

除解释规范的上下文定义外,为错误。

字符串集合

{"toml"}

dependency_groups

除解释规范的上下文定义外,为错误。

字符串集合

{"test"}

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 508 定义,取代了 PEP 345 中的环境标记部分。

完整语法

完整的 parsley 语法

wsp           = ' ' | '\t'
version_cmp   = wsp* <'<=' | '<' | '!=' | '==' | '>=' | '>' | '~=' | '==='>
version       = wsp* <( letterOrDigit | '-' | '_' | '.' | '*' | '+' | '!' )+>
version_one   = version_cmp:op version:v wsp* -> (op, v)
version_many  = version_one:v1 (',' version_one)*:v2 (',' wsp*)? -> [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' | 'extras' | 'dependency_groups' # 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,",
    "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))

历史

  • 2015 年 11 月:此规范通过 PEP 508 批准。

  • 2019 年 7 月:python_version 的定义从 platform.python_version()[:3] 更改'.'.join(platform.python_version_tuple()[:2]),以适应未来可能出现的具有两位主次版本号的 Python 版本(例如 3.10)。[3]

  • 2024 年 6 月:version_many 的定义已更改,允许使用尾随逗号,与自 2022 年底以来一直在使用的 Python 实现的行为相符。

  • 2025 年 4 月:为 pylock.toml 规范 添加了 extrasdependency_groups,通过 PEP 751 批准。

  • 2025 年 8 月:建议的名称验证正则表达式已修复,以匹配字段规范(它之前以 $ 而不是 \Z 结尾,错误地允许尾随换行符)

参考