PEP 508 – Dependency specification for Python Software Packages
- Author:
- Robert Collins <rbtcollins at hp.com>
- BDFL-Delegate:
- Donald Stufft <donald at stufft.io>
- Discussions-To:
- Distutils-SIG list
- Status:
- Final
- Type:
- Standards Track
- Topic:
- Packaging
- Created:
- 11-Nov-2015
- Post-History:
- 05-Nov-2015, 16-Nov-2015
- Resolution:
- Distutils-SIG message
摘要
这个 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 中可能会对 name 进行完全的重新定义。regex(使用 re.IGNORECASE 运行)是:
^([A-Z0-9]|[A-Z0-9][A-Z0-9._-]*[A-Z0-9])$
额外组件
额外内容是发行版的可选部分。发行版可以指定任意多的附加项,当在依赖项规范中使用附加项时,每个附加项都会导致声明该发行版的附加依赖项。例如:
requests[security]
额外组件在它们定义的依赖项中与它们所连接的发行版的依赖项相结合。上面的例子将导致请求被安装,并且请求拥有自己的依赖项,以及在请求的 “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 中描述的设计中观察到的一些问题。
标记表达式中的比较是由比较运算符输入的。<marker_op> 算子不在 <version_cmp> 执行与 Python 中处理字符串相同的操作。<version_cmp> 当定义了 PEP 440 版本比较规则时(即两端都有有效的版本说明符时),算子使用 PEP 440 版本比较规则。如果没有定义的 PEP 440 行为,并且该操作符存在于 Python 中,则该算子将退回到 Python 行为。否则将引发错误。例如,以下将导致错误:
"dog" ~= "fred"
python_version ~= "surprise"
用户提供的常量总是被编码为带有 '
或 "
引号的字符串。注意,反斜杠转义没有定义,但是现有的实现支持它们。它们没有包括在本规范中,因为它们增加了复杂性,而且目前还没有明显的需求。类似地,我们没有定义非 ASCII 字符支持:我们引用的所有运行时变量都希望是仅 ASCII 的。
标记语法中的变量,如 “os_name”,解析为在 Python 运行时中查找的值。除了 “extra” 之外,所有的值都在现在所有的 Python 版本上定义了——如果值没有定义,这是标记实现中的错误。
未知变量必须引发错误,而不是导致比较结果为 True 或 False。
在给定的 Python 实现中不能计算值的变量对于某些版本应该计算为 0
,对于所有其他变量应该计算为空字符串。
“extra” 变量是特殊的。轮子使用它来指示 wheel METADATA
文件中哪些规范适用于给定的额外元素,但由于 METADATA
文件基于 PEP 426 的草案版本,因此目前没有对此的规范。无论如何,在发生这种特殊处理的上下文之外,”extra” 变量应该像所有其他未知变量一样导致错误。
标记 | Python等效 | 样例值 |
---|---|---|
os_name |
os.name |
posix , java |
sys_platform |
sys.platform |
linux , linux2 , darwin , java1.8.0_51 (note that “linux”
is from Python3 and “linux2” from 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 |
see definition below | 3.4.0 , 3.5.0b1 |
extra |
An error except when defined by the context interpreting the specification. | 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 的大部分已经被广泛部署,因此不存在兼容性问题。
然而,PEP 与部署的 base 有一些不同之处。
首先,PEP 440 直接引用实际上还没有正式部署,但它们被设计为可兼容添加的,并且将它们添加到 pip 或其他使用发行版中现有依赖项元数据的工具中并没有已知的障碍——特别是因为它们无论如何都不允许出现在 PyPI 上传的发行版中。
其次,PEP 426 标记已经进行了一些合理的部署,特别是在 wheels 和 pip 中,它将以不同的方式处理与 python_full_version
“2.7.10” 的版本比较。特别是在 426 中,”2.7.10” 小于 “2.7.9”。这种向后不兼容是故意的。还定义了新的算子—— “~=” 和 “===”,以及新的变量—— platform_release
, platform_system
, implementation_name
和 implementation_version
,这些在旧的标记实现中是不存在的。变量将在这些实现上出错。这两个特性的用户都需要做出判断,当支持在生态系统中足够广泛时,使用它们不会引起兼容性问题。
基本原理
为了推进任何依赖于环境标记的新 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])
,以适应具有两位主版本和副版本的 Python 的潜在未来版本(例如 3.10)。 [3]
References
Copyright
This document has been placed in the public domain.
Source: https://github.com/python/peps/blob/main/pep-0508.txt
Last modified: 2022-06-21 21:47:58 GMT