PEP 508 – Python 軟體套件的相依性規範
- 作者:
- Robert Collins <rbtcollins at hp.com>
- BDFL-Delegate:
- Donald Stufft <donald at stufft.io>
- 討論於:
- Distutils-SIG 郵件列表
- 狀態:
- 最終 (Final)
- 類型:
- 標準軌跡 (Standards Track)
- 主題:
- 套件封裝 (Packaging)
- 建立日期:
- 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
概念
相依性規範總是指定一個發布名稱 (distribution name)。它可以包含「額外功能」(extras),用以擴展所指定發布版本的相依性,從而啟用選擇性功能。安裝的版本可以透過版本限制來控制,或透過提供特定產物的 URL 來安裝。最後,相依性可以使用環境標記 (environment markers) 設定為條件式的。
語法
我們將先簡要介紹語法,隨後深入探討每個部分的語意。
發布規範以 ASCII 文字編寫。我們使用 parsley [2] 語法來提供精確的語法定義。預計該規範將被嵌入到一個更大的系統中,該系統提供註解、透過接續符號支援多行,或此類其他功能。
包含用於構建有用剖析樹 (parse tree) 的註釋的完整語法已包含在 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 對名稱實施嚴格限制——它們必須符合不區分大小寫的 regex,否則將不被接受。因此,在本 PEP 中,我們將識別碼的可接受值限制為該 regex。未來可能會在元資料 PEP 中對名稱進行全面重新定義。(使用 re.IGNORECASE 執行)該 regex 為
^([A-Z0-9]|[A-Z0-9][A-Z0-9._-]*[A-Z0-9])$
額外功能 (Extras)
「額外功能」(extra) 是發布版本的選用部分。發布版本可以指定任意數量的額外功能,並且當額外功能用於相依性規範時,每一個額外功能都會導致宣告該發布版本的額外相依性。例如
requests[security]
Extras 會將它們定義的相依性與所附加發布版本的相依性進行聯集 (union)。上述範例將導致安裝 requests,以及 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 所述設計中觀察到的一些問題。
標記運算式中的比較由比較運算子決定型別。不在 <version_cmp> 中的 <marker_op> 運算子,其執行方式與 Python 中的字串比較相同。<version_cmp> 運算子在已定義的情況下(即兩側皆有有效的版本指定符),使用 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」變數應像所有其他未知變數一樣導致錯誤。
| 標記 (Marker) | 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 的發布版本中。
其次,已經有相當程度部署(特別是在 Wheels 和 pip 中)的 PEP 426 標記,在處理與 python_full_version “2.7.10” 的版本比較時會有不同行為。具體來說,在 426 中,“2.7.10” 小於 “2.7.9”。這種向後不相容是有意為之的。我們也定義了新的運算子 “~=” 和 “===”,以及舊標記實作中不存在的新變數:platform_release, platform_system, implementation_name 和 implementation_version。這些變數在那些實作上會報錯。這兩個功能的使用者需要自行判斷生態系統中的支援度是否已廣泛到足以使用它們而不會導致相容性問題。
第三,PEP 345 要求版本指定符周圍必須加括號。為了接受 PEP 345 的相依性規範,我們接受括號,但不應主動產生它們。
原理
為了推動任何依賴環境標記的新 PEP,我們需要一個包含這些標記現代形式的規範。本 PEP 將所有目前未指定的元件整理為一個規範形式。
需求指定符採用自 setuptools pkg_resources 文件中的 EBNF,因為我們希望避免依賴事實上的標準 (de facto standard) 而非 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]
參考文獻
版權
此文件已歸入公有領域 (public domain)。
來源:https://github.com/python/peps/blob/main/peps/pep-0508.rst
最後修改日期:2025-02-20 11:58:35 GMT