Following system colour scheme Selected dark colour scheme Selected light colour scheme

Python 개선 제안 한국어 번역

PEP 386 – Distutils의 버전 비교 모듈 변경

Author:
Tarek Ziadé <tarek at ziade.org>
Status:
Superseded
Type:
Standards Track
Topic:
Packaging
Created:
04-Jun-2009
Superseded-By:
440

Table of Contents

번역·라이선스 안내

이 비공식 한국어 번역은 원문 Copyright 절의 Public Domain 조건에 따라 제공합니다. 원저자와 공식 원문은 그대로 표시합니다. 수정되지 않은 기준 원문 · 공식 최신판

초록

참고: 이 PEP는 PEP 440에 정의된 버전 식별 및 의존성 명세 체계로 대체되었습니다.

이 PEP는 Distutils에 새로운 버전 비교 스키마 시스템을 제안했습니다.

동기

Python에서는 프로젝트가 버전을 관리하는 방법과 버전을 증가시키는 방법에 아직 실질적인 제한이 없습니다.

Distutils는 version배포 메타데이터 필드를 제공하지만 자유 형식이며, PyPI와 같은 현재 사용자들은 일반적으로 예상되는 의미와 관계없이 가장 최근에 게시된 버전을 latest버전으로 간주합니다.

Distutils는 곧 배포 패키지가 Requires-Dist 메타데이터 필드를 통해 다른 배포 패키지에 대한 의존성을 표현할 수 있도록 기능을 확장할 예정이며(PEP 345참조), 선택적으로 이 필드를 사용하여 의존성을 호환 가능한 버전 집합으로 제한할 수 있도록 할 예정입니다. 이 필드는 모듈과 패키지에 대한 의존성을 표현하던 Requires를 대체한다는 점에 유의하십시오.

Requires-Dist 필드를 사용하면 배포 패키지가 다른 패키지에 대한 의존성을 정의하고 선택적으로 이 의존성을 호환 가능한 버전 집합으로 제한할 수 있으므로, 다음과 같이 작성할 수 있습니다.:

Requires-Dist: zope.interface (>3.5.0)

이는 해당 배포 패키지가 3.5.0보다 높은 버전의 zope.interface를 필요로 한다는 의미입니다.

이는 또한 Python 프로젝트가 자신을 설치하는 데 사용될 도구와 동일한 규칙을 따라야 버전을 비교할 수 있다는 의미입니다.

따라서 이 PEP는 상호 운용성을 위해 버전 정보를 표현하고 그 비교 의미 체계를 정의하는 표준 스키마를 제안합니다.

또한 현재로서는 두 배포 패키지 버전을 어떻게 비교해야 할지 결정하기 어려울 수 있으므로, 이를 통해 운영 체제 패키지 관리자가 표준을 준수하는 배포 패키지를 다시 패키징하는 작업이 더 쉬워집니다.

요구 사항 및 현재 상태

이 PEP의 범위에는 기존 버전 관리 스키마의 전부 또는 대부분을 지원하기 위한 범용 버전 관리 스키마를 제공하는 것이 포함되지 않습니다. 배포판 또는 프로젝트 정책에 의해 요구되거나, 변경될 것으로 기대할 수 없는 역사적 이유로 인해 서로 경쟁하는 문법은 언제나 존재합니다.

제안된 스키마는 일반적인 버전 관리 의미 체계를 표현할 수 있어야 하며, 따라서 어떤 대체 버전 관리 스키마든 구문 분석하여 호환되는 스키마로 변환할 수 있어야 합니다. 이는 운영 체제 패키지 관리자가 일반적으로 기존 버전 스키마를 처리하는 방식이며, 임의의 버전 관리 스키마 집합을 지원하는 것보다 바람직한 대안입니다.

일반적인 관행과 규칙을 준수하고 단순성을 갖추는 것은 원활한 채택과 고통 없는 전환을 촉진한다는 점에서 장점입니다. 때로는 순수성보다 실용성이 우선합니다.

프로젝트마다 버전 관리 요구 사항은 매우 다르지만, 다음 의미 체계는 중요하다고 널리 여겨집니다.

  1. 둘 이상의 버전 관리 수준을 표현할 수 있어야 합니다(일반적으로 주 버전과 부 버전으로 표현하며, 때로는 마이크로 버전도 표현합니다).
  2. 상당수의 프로젝트에는 “pre-releases”(예: “alpha”, “beta”, “rc”)를 나타내는 특별한 의미의 버전이 필요하며, 여기에는 널리 사용되는 별칭도 있습니다(“a”는 “alpha”, “b”는 “beta”, “c”는 “rc”를 의미합니다). 또한 이러한 사전 릴리스 버전으로 인해 버전 문자열 구성 요소에 단순한 영숫자 순서를 사용할 수 없습니다. (예: 3.1a1 < 3.1)
  3. 일부 프로젝트에는 일반 버전의 “post-releases”도 필요하며, 주로 그렇지 않으면 명확하게 표현할 수 없는 설치 관리자 작업에 사용됩니다.
  4. 개발 버전을 사용하면 패키지 관리자가 아직 릴리스되지 않은 작업에 대해 이후의 정식 릴리스와 버전이 충돌하는 것을 피할 수 있습니다.

더 나아가 버전 번호를 관리하는 도구를 사용하려는 사람들에게 주요 도구는 다음 두 가지입니다.

  • 현재 Distutils 시스템 [1]
  • Setuptools [2]

Distutils

Distutils는 현재 버전을 관리하는 데 사용할 수 있는 StrictVersion클래스와 LooseVersion클래스를 제공합니다.

LooseVersion 클래스는 상당히 느슨합니다. Distutils 문서에서 인용합니다.:

Version numbering for anarchists and software realists.
Implements the standard interface for version number classes as
described above.  A version number consists of a series of numbers,
separated by either periods or strings of letters.  When comparing
version numbers, the numeric components will be compared
numerically, and the alphabetic components lexically.  The following
are all valid version numbers, in no particular order:

    1.5.1
    1.5.2b2
    161
    3.10a
    8.02
    3.4j
    1996.07.12
    3.2.pl0
    3.1.1.6
    2g6
    11g
    0.960923
    2.2beta29
    1.13++
    5.5.kw
    2.0b1pl0

In fact, there is no such thing as an invalid version number under
this scheme; the rules for comparison are simple and predictable,
but may not always give the results you want (for some definition
of "want").

이 클래스는 모든 버전 문자열을 유효한 것으로 취급하고, 이를 먼저 수치순으로 정렬한 다음 사전순으로 정렬하는 알고리즘을 제공합니다. 이는 프로젝트의 버전을 정하는 데 무엇이든 사용할 수 있다는 의미입니다.:

>>> from distutils.version import LooseVersion as V
>>> v1 = V('FunkyVersion')
>>> v2 = V('GroovieVersion')
>>> v1 > v2
False

이 방식의 문제는 모든 중첩 수준을 표현할 수 있지만, 요구 사항 2, 3, 4에서 설명한 것처럼 버전에 특별한 의미(프리릴리스와 포스트릴리스뿐 아니라 개발 버전)를 부여할 수 없다는 점입니다.

StrictVersion 클래스는 더 엄격합니다. 문서에서 인용합니다.:

Version numbering for meticulous retentive and software idealists.
Implements the standard interface for version number classes as
described above.  A version number consists of two or three
dot-separated numeric components, with an optional "pre-release" tag
on the end.  The pre-release tag consists of the letter 'a' or 'b'
followed by a number.  If the numeric components of two version
numbers are equal, then one with a pre-release tag will always
be deemed earlier (lesser) than one without.

The following are valid version numbers (shown in the order that
would be obtained by sorting according to the supplied cmp function):

    0.4       0.4.0  (these two are equivalent)
    0.4.1
    0.5a1
    0.5b3
    0.5
    0.9.6
    1.0
    1.0.4a3
    1.0.4b1
    1.0.4

The following are examples of invalid version numbers:

    1
    2.7.2.2
    1.3.a4
    1.3pl1
    1.3c4

이 클래스는 몇 가지 규칙을 강제하며, 버전 번호를 다루는 데 적절한 도구를 제공합니다.:

>>> from distutils.version import StrictVersion as V
>>> v2 = V('GroovieVersion')
Traceback (most recent call last):
...
ValueError: invalid version number 'GroovieVersion'
>>> v2 = V('1.1')
>>> v3 = V('1.3')
>>> v2 < v3
True

프리릴리스 버전과 어느 정도의 구조를 추가하지만, 요구 사항 3과 4에서 설명한 개발 릴리스나 포스트릴리스 태그와 같은 일부 의미론적 요소가 없어 실제로 사용하기에는 부족합니다.

또한 Distutils의 버전 클래스가 수년 동안 존재해 왔지만 커뮤니티에서 실제로 사용되지는 않는다는 점에 유의하십시오.

Setuptools

Setuptools는 또 다른 버전 비교 도구 [3]를 제공하며, 버전에 어떤 규칙도 강제하지 않지만 parse_version 함수를 사용하여 문자열을 정렬 가능한 키로 변환하는 더 나은 알고리즘을 제공하려고 합니다.

문서에서 인용합니다.:

Convert a version string to a chronologically-sortable key

This is a rough cross between Distutils' StrictVersion and LooseVersion;
if you give it versions that would work with StrictVersion, then it behaves
the same; otherwise it acts like a slightly-smarter LooseVersion. It is
*possible* to create pathological version coding schemes that will fool
this parser, but they should be very rare in practice.

The returned value will be a tuple of strings.  Numeric portions of the
version are padded to 8 digits so they will compare numerically, but
without relying on how numbers compare relative to strings.  Dots are
dropped, but dashes are retained.  Trailing zeros between alpha segments
or dashes are suppressed, so that e.g. "2.4.0" is considered the same as
"2.4". Alphanumeric parts are lower-cased.

The algorithm assumes that strings like "-" and any alpha string that
alphabetically follows "final"  represents a "patch level".  So, "2.4-1"
is assumed to be a branch or patch of "2.4", and therefore "2.4.1" is
considered newer than "2.4-1", which in turn is newer than "2.4".

Strings like "a", "b", "c", "alpha", "beta", "candidate" and so on (that
come before "final" alphabetically) are assumed to be pre-release versions,
so that the version "2.4" is considered newer than "2.4a1".

Finally, to handle miscellaneous cases, the strings "pre", "preview", and
"rc" are treated as if they were "c", i.e. as though they were release
candidates, and therefore are not as new as a version string that does not
contain them, and "dev" is replaced with an '@' so that it sorts lower
than any other pre-release tag.

다시 말해, parse_version은 각 버전 문자열에 대해 StrictVersion과 호환되는 튜플을 반환하면서도 임의의 버전을 받아들이고 비교할 수 있도록 처리합니다.:

>>> from pkg_resources import parse_version as V
>>> V('1.2')
('00000001', '00000002', '*final')
>>> V('1.2b2')
('00000001', '00000002', '*b', '00000002', '*final')
>>> V('FunkyVersion')
('*funkyversion', '*final')

이 체계에서는 순수성보다 실용성이 우선되지만, 그 결과 어떤 정책도 강제하지 않으며 명확한 표준이 없기 때문에 매우 복잡한 의미론으로 이어집니다. 널리 사용되는 관례에 적응하려고 할 뿐입니다.

기존 시스템의 문제점

앞서 설명한 버전 비교 도구의 가장 큰 문제는 지나치게 관대하면서도 동시에 필요한 의미론의 일부를 표현할 수 없다는 점입니다. PyPI [4]에 있는 많은 버전은 명백히 유용하지 않은 버전이므로, 사용자가 특정 패키지에서 사용한 버전 관리 방식을 파악하고 PyPI를 기반으로 도구를 제공하기가 어렵습니다.

Distutils 클래스는 Python 프로젝트에서 실제로 사용되지 않지만, Setuptools 함수는 특정 프로젝트의 종속성을 설치하는 easy_install [6], pip [5] 또는 zc.buildout [7] 같은 도구에서 사용되기 때문에 상당히 널리 퍼져 있습니다.

Setuptools가 실제로 버전을 비교하고 정렬하는 메커니즘을 제공하기는 하지만, 어떤 코드를 실행해 보지 않고도 사람이 그 정렬을 합리적으로 시도할 수 있는 방식으로 버전 관리 사양이 정의되어 있다면 훨씬 바람직합니다.

또한 RPM에서 날짜를 “major” 버전 번호(예: 버전 문자열 “20090421”)로 사용하는 데 문제가 있습니다. 일반적인 “major.minor…” 버전 체계로 전환하려는 시도는 항상 “20090421”보다 낮게 정렬되므로 문제가 됩니다.

마지막으로 -의 의미는 Setuptools에만 해당하는 반면, Debian이나 Ubuntu에서 사용하는 시스템과 같은 일부 패키징 시스템에서는 이를 사용하지 않습니다.

새로운 버전 관리 알고리즘

Pycon에서 Python, Ubuntu 및 Fedora 커뮤니티의 구성원들은 모두가 받아들일 수 있는 버전 표준을 마련하기 위해 작업했습니다.

현재 이 표준은 verlib라고 불리며, 프로토타입은 [9]에 있습니다.

지원되는 의사 형식은 다음과 같습니다.:

N.N[.N]+[{a|b|c|rc}N[.N]+][.postN][.devN]

실제 정규 표현식은 다음과 같습니다.:

expr = r"""^
(?P<version>\d+\.\d+)         # minimum 'N.N'
(?P<extraversion>(?:\.\d+)*)  # any number of extra '.N' segments
(?:
    (?P<prerel>[abc]|rc)         # 'a' = alpha, 'b' = beta
                                 # 'c' or 'rc' = release candidate
    (?P<prerelversion>\d+(?:\.\d+)*)
)?
(?P<postdev>(\.post(?P<post>\d+))?(\.dev(?P<dev>\d+))?)?
$"""

몇 가지 예를 들면 더 명확해질 것입니다.:

>>> from verlib import NormalizedVersion as V
>>> (V('1.0a1')
...  < V('1.0a2.dev456')
...  < V('1.0a2')
...  < V('1.0a2.1.dev456')
...  < V('1.0a2.1')
...  < V('1.0b1.dev456')
...  < V('1.0b2')
...  < V('1.0b2.post345')
...  < V('1.0c1.dev456')
...  < V('1.0c1')
...  < V('1.0.dev456')
...  < V('1.0')
...  < V('1.0.post456.dev34')
...  < V('1.0.post456'))
True

뒤에 붙는 .dev123은 사전 릴리스를 위한 것입니다. .post123은 사후 릴리스를 위한 것으로 – Twisted [8]처럼 실제로 여러 프로젝트에서 사용되고 있습니다. 예를 들어, 1.2.0 릴리스 이후1.2.0-r678 릴리스가 있을 수 있습니다. r이 사전 릴리스인지 사후 릴리스인지 나타내는 데 모호하기 때문에, r 대신 post를 사용했습니다.

.post456.dev34는 사후 릴리스의 개발 마커를 나타내며, .post456 마커보다 앞에 정렬됩니다. 이는 사후 릴리스의 개발 버전을 만드는 데 사용할 수 있습니다.

사전 릴리스는 “alpha”를 위한 a, “beta”를 위한 b, “release candidate”를 위한 c를 사용할 수 있습니다. rc는 버전 체계를 파이썬 자체의 버전 체계와 호환되게 하기 위해 추가된 “release candidate”의 대체 표기법입니다. rcc 뒤에 정렬됩니다:

>>> from verlib import NormalizedVersion as V
>>> (V('1.0a1')
...  < V('1.0a2')
...  < V('1.0b3')
...  < V('1.0c1')
...  < V('1.0rc2')
...  < V('1.0'))
True

c가 서드파티 프로젝트에 선호되는 마커라는 점에 유의하십시오.

verlibNormalizedVersion 클래스와 suggest_normalized_version 함수를 제공합니다.

NormalizedVersion

NormalizedVersion 클래스는 버전을 보관하고 다른 버전과 비교하는 데 사용됩니다. 이는 버전의 표현을 담은 문자열을 인자로 받습니다:

>>> from verlib import NormalizedVersion
>>> version = NormalizedVersion('1.0')

버전은 문자열로 표현할 수 있습니다:

>>> str(version)
'1.0'

또는 다른 버전과 비교할 수 있습니다:

>>> NormalizedVersion('1.0') > NormalizedVersion('0.9')
True
>>> NormalizedVersion('1.0') < NormalizedVersion('1.1')
True

버전을 구성하는 부분들을 제공하여 인스턴스를 생성하고자 한다면 from_parts라는 클래스 메서드를 사용할 수 있습니다.

예제

>>> version = NormalizedVersion.from_parts((1, 0))
>>> str(version)
'1.0'

>>> version = NormalizedVersion.from_parts((1, 0), ('c', 4))
>>> str(version)
'1.0c4'

>>> version = NormalizedVersion.from_parts((1, 0), ('c', 4), ('dev', 34))
>>> str(version)
'1.0c4.dev34'

suggest_normalized_version

suggest_normalized_version은 주어진 버전 문자열에 가까운 정규화된 버전을 제안하는 함수입니다. 정규화되지 않은 버전 문자열(즉, NormalizedVersion이 마음에 들어 하지 않는 문자열)이 있다면, 이 함수를 통해 동등한(또는 근접한) 정규화된 버전을 얻을 수 있습니다.

이는 현재 PyPI에서 사용 중인 버전들을 관찰한 결과를 바탕으로, 주어진 문자열에 여러 가지 단순한 정규화를 적용합니다.

2010년 1월 6일에 이러한 버전들을 덤프한 결과, 이 함수는 PyPI가 보유한 8821개의 배포 패키지 중 다음과 같은 결과를 냈습니다:

  • 7822개(88.67%)는 변경 없이도 이미 NormalizedVersion과 일치합니다
  • 717개(8.13%)는 이 제안 방법을 사용했을 때 일치합니다
  • 282개(3.20%)는 전혀 일치하지 않습니다.

NormalizedVersion과 호환되지 않으며 호환 가능한 형태로 변환할 수 없는 3.20%의 프로젝트는, 대부분 날짜 기반 버전 체계, 커스텀 마커가 있는 버전, 또는 더미 버전입니다. 예:

  • working proof of concept
  • 1 (first draft)
  • unreleased.unofficialdev
  • 0.1.alphadev
  • 2008-03-29_r219
  • 등등.

도구가 버전을 다뤄야 할 때, 버전 문자열에 suggest_normalized_version을 사용하는 것이 한 가지 전략입니다. 이 함수가 None을 반환하면, 제공된 버전이 표준 스킴에 충분히 가깝지 않다는 것을 의미합니다. 원래 버전과 약간 다른 버전을 반환한다면, 그것은 제안된 정규화된 버전입니다. 마지막으로, 동일한 문자열을 반환한다면, 버전이 스킴과 일치한다는 것을 의미합니다.

사용 예시는 다음과 같습니다:

>>> from verlib import suggest_normalized_version, NormalizedVersion
>>> import warnings
>>> def validate_version(version):
...     rversion = suggest_normalized_version(version)
...     if rversion is None:
...         raise ValueError('Cannot work with "%s"' % version)
...     if rversion != version:
...         warnings.warn('"%s" is not a normalized version.\n'
...                       'It has been transformed into "%s" '
...                       'for interoperability.' % (version, rversion))
...     return NormalizedVersion(rversion)
...

>>> validate_version('2.4-rc1')
__main__:8: UserWarning: "2.4-rc1" is not a normalized version.
It has been transformed into "2.4c1" for interoperability.
NormalizedVersion('2.4c1')

>>> validate_version('2.4c1')
NormalizedVersion('2.4c1')

>>> validate_version('foo')
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "<stdin>", line 4, in validate_version
ValueError: Cannot work with "foo"

로드맵

Distutils는 기존 버전 클래스를 폐기하고 대신 NormalizedVersion을 사용할 예정입니다. 이 PEP에서 소개된 verlib 모듈은 version으로 이름이 변경되어 distutils 패키지에 배치될 예정입니다.

참고 문헌

감사의 말

Trent Mick, Matthias Klose, Phillip Eby, David Lyon, 그리고 Pycon과 Distutils-SIG의 많은 사람들.