PEP 508 – Python 소프트웨어 패키지를 위한 의존성 사양
- 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
번역·라이선스 안내
이 비공식 한국어 번역은 원문 Copyright 절의 Public Domain 조건에 따라 제공합니다. 원저자와 공식 원문은 그대로 표시합니다. 수정되지 않은 기준 원문 · 공식 최신판
초록
이 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의 자체 의존성과 requests의 “security” 추가 기능에 나열된 모든 의존성도 설치됩니다.
여러 추가 기능이 나열되면 모든 의존성이 함께 합쳐집니다.
버전
버전 번호와 버전 비교에 관한 자세한 내용은 PEP 440을 참조하십시오. 버전 명세는 사용할 수 있는 배포 패키지의 버전을 제한합니다. 버전 명세는 URL을 통해 조회되는 배포 패키지가 아니라 이름으로 조회되는 배포 패키지에만 적용됩니다. 버전 비교는 마커 기능에서도 사용됩니다. 버전 주위의 선택적 대괄호는 PEP 345와의 하위 호환성을 위해 존재하지만, 생성해서는 안 되고 받아들이기만 해야 합니다.
환경 마커
환경 마커를 사용하면 의존성 명세가 해당 의존성을 언제 사용해야 하는지를 설명하는 규칙을 제공할 수 있습니다. 예를 들어 argparse가 필요한 패키지를 생각해 보십시오. Python 2.7에서는 argparse가 항상 존재합니다. 이전 Python 버전에서는 argparse를 의존성으로 설치해야 합니다. 이는 다음과 같이 표현할 수 있습니다.:
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” 변수는 특수합니다. 이 변수는 휠의 METADATA 파일에서 특정 extra에 적용되는 사양을 휠이 표시하는 데 사용되지만, 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 |
사양을 해석하는 맥락에서 정의되지 않는 한 오류입니다. | 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가 배포된 기반과 다른 몇 가지 지점이 있습니다.
첫째, PEP 440 직접 참조는 실제로 아직 널리 배포되지 않았지만, 호환성을 유지하면서 추가할 수 있도록 설계되었으며, 배포판의 기존 의존성 메타데이터를 사용하는 pip 또는 기타 도구에 이를 추가하는 데 알려진 장애물은 없습니다. 특히 PyPI에 업로드되는 배포판에는 어차피 이러한 참조가 허용되지 않기 때문입니다.
둘째, 특히 휠과 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에서 채택했습니다. 사실상 표준이지만 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])로 변경되었습니다. 이는 2자리 주 버전 및 부 버전을 사용하는 잠재적인 향후 Python 버전(예: 3.10)에 대비하기 위한 것입니다. [3]
참고 문헌
Copyright
This document has been placed in the public domain.