依存関係指定子#

元々は PEP 508 で指定されていた依存関係指定子のフォーマットを、この説明文書は記述します。

依存関係 <dependency> の任務は、 pip [1] のようなツールがインストールするべき正しいパッケージを探し出すことができるようにすることです。これは時には大変に曖昧で名称を指定するだけであったり、別の時には非常に限定的でインストールするべき特定のファイルを参照したりします。場合によっては、依存関係 <dependency> がひとつのプラットフォームでのみ妥当であったり、いくつかのバージョンだけが受け入れ可能であったりするので、(依存関係 <dependency> を記述する) 言語としてはこれらすべてのケースを記述できるものでなければなりません。

定義された言語は、簡潔な行単位のフォーマットであって pip の requirements ファイルで既に広く使われているものですが、そのようなファイル群を許容するようなコマンドラインオプションを指定することはしていません。ひとつ注意しなければならないのは、 バージョン指定子仕様 で指定されているような 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

概念#

依存関係の指定では、常に、配布物の名前を指定します。名前で指定された配布物で特定の追加機能を有効にするように依存関係を拡張するような追加物 <extra> を含んでいても構いません。インストールされたバージョンをバージョンリミットで制御することもできますし、特定のアーティファクトをインストールするために URL を与えることもできます。依存関係は最終的に環境マーカを用いて条件別に作成することもできます。

文法#

最初に文法について簡単に触れた後、それぞれの節の意味論 <semantics> について深く掘り下げることにしましょう。

配布物の仕様は ASCII テキストで書かれています。厳密な文法としては parsley [2] の文法を使っています。この仕様は、コメントや継続による複数行サポートやその他の機能の枠組みを与えるもっと大きなシステムの中に組み込まれることを期待しています。

役に立つ構文解析ツリーを構成するための注釈機能を含む完全な文法は、この説明文書の末尾に置きました。

バージョン指定子仕様 の規則に従ってバージョンを指定しても構いません。(ノート: 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* ']'

追加物の名前に対する制限事項は 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*

空白文字 <Whitespace>#

行を分割するものではない空白文字には特に意味はなく、ほとんどの場合には必須ではないものです。唯一の例外は、 URL による要求事項の末尾を検出するためのものです。

名前 <Names>#

Python の配布物の名前は、現時点では PEP 345 で定義されています。名前は配布物の最も基本的な識別子として働きます。(名前は) あらゆる依存関係の指定に出現し、それだけで十分に指定することができます。しかしながら、 PyPI では名前に厳密な制約を課しています - 名前は大文字小文字を区別しない正規表現に合致しなければ受け入れられません。従って、この説明文書では、その正規表現に合致する識別子だけを受け入れ可能な値として扱うことにしましょう。名前の完全な再定義はメタデータ PEP として将来に出現するかもしれません。ここでいう (re.IGNORECASE とともに評価されるべき) 正規表現とは、次のようなものです:

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

追加物 <Extras>#

追加物とは、配布物の必須ではない部分のことです。配布物では追加物を幾つでも指定することができ、追加物が依存関係の指定場所で使われた 場合 には、それぞれの追加物が配布物の追加的な依存関係を宣言する結果になります。例えば:

requests[security,tests]

追加物 <extras> の依存関係の合併とは、その追加物が添付されている配布物 <distribution> の依存関係と一緒に定義されることです。上に示した例では、結果として requests がインストールされることになり、requests は自身の依存関係を持つので requests の "security" 追加物 <extra> に列挙されたすべての依存関係 (先) もインストールされることになります。

複数の追加物 <extra> が列挙されている場合には、すべての依存関係の合併集合が依存関係になります。

バージョン指定子#

バージョン番号やその比較方法について、詳しくは バージョン指定子仕様 をみてください。バージョン仕様は、配布物のバージョンとして使うことができる範囲を定めています。これは、名前によって参照される配布物にのみ適用されるのであって、URL を通じて指定されるものには該当しません。バージョン番号の比較は、また、マーカー機能においても使われます。バージョンの周囲にある必須でない括弧は PEP 345 との互換性を保つために存在していますが、そのようなものを生成すべきではなくて受容するだけにとどめるべきです。

環境マーカ#

環境マーカは、依存関係の指定においてその依存関係がいつ使われるべきであるかを記述する規則を提供します。例えば、あるパッケージが argparse を必要とするとしましょう。Python 2.7 では argparse は常に存在します。もっと古いバージョンの Python では依存関係としてインストールされなければなりません。これは次のように表現することができます:

argparse;python_version<"2.7"

マーカ表現は評価されると真か偽に帰着します。偽と評価された場合には、その依存関係の指定は無視されなければなりません。

マーカ言語は 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" 変数は、扱いが特別です。それは wheel ファイルにおいて、その wheel の METADATA ファイル内の特定の追加物 <extra> にどの仕様を適用するべきであるかを知らせるために使われますが、 METADATA ファイルの様式が PEP 426 のドラフトバージョンに基づいているので、現時点ではその仕様が存在していないのです。それにも関わらず、この特別な扱いが行われる場所ではない文脈においては、 "extra" 変数はその他の未知の変数と同様にエラーに帰着するべきです。

マーカ <Marker>

Python 同等物

値の例

OS の名称 <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

仕様を通訳する文脈で定義された場合を除くエラー。

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 (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))

歴史#

  • 2015年11月: PEP 508 を通じてこの仕様が承認されました。

  • 2019年6月: python_version の定義は、Python の将来のバージョンが二桁のメジャーバージョンやマイナーバージョンを持つ場合 (例えば 3.10) でもそれを収容できるように、 platform.python_version()[:3] から '.'.join(platform.python_version_tuple()[:2])変更されました[3]

参考文献#