PEP 386 – Distutils의 버전 비교 모듈 변경
- Author:
- Tarek Ziadé <tarek at ziade.org>
- Status:
- Superseded
- Type:
- Standards Track
- Topic:
- Packaging
- Created:
- 04-Jun-2009
- Superseded-By:
- 440
번역·라이선스 안내
이 비공식 한국어 번역은 원문 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는 상호 운용성을 위해 버전 정보와 그 비교 의미를 표현하는 표준 스키마를 제안합니다.
게다가 현재로서는 두 배포 패키지 버전을 어떻게 비교해야 할지 판단하기 어려울 수 있으므로, 이 스키마는 OS 패키저가 표준을 준수하는 배포 패키지를 다시 패키징하는 작업을 더 쉽게 만들어 줄 것입니다.
요구 사항과 현재 상황
기존 버전 관리 스키마 전부 또는 대부분을 지원하려는 범용 버전 관리 스키마를 제공하는 것은 이 PEP의 범위가 아닙니다. 배포판이나 프로젝트 정책이 강제하거나, 바뀌기를 기대할 수 없는 역사적 이유로 인해 경쟁하는 문법은 언제나 존재할 것입니다.
제안하는 스키마는 일반적인 버전 관리 의미를 표현할 수 있어야 하며, 그래야 어떤 대안 버전 관리 스키마든 파싱하여 규격에 맞는 스키마로 변환할 수 있습니다. 이는 OS 패키저가 기존 버전 스키마를 다루는 일반적인 방식이며, 임의의 버전 관리 스키마 집합을 지원하는 것보다 바람직한 대안입니다.
마찰 없는 도입과 수월한 전환을 위해, 일반적인 관행과 규칙을 따르는 것과 단순함은 장점이 됩니다. 때로는 실용성이 순수성을 이깁니다.
프로젝트마다 버전 관리 요구 사항은 매우 다르지만, 다음은 중요한 의미로 널리 여겨집니다:
- 둘 이상의 버전 관리 수준을 표현할 수 있어야 합니다(보통 메이저 리비전과 마이너 리비전으로 표현하며, 때로는 마이크로 리비전도 사용합니다).
- 상당수의 프로젝트에는 “사전 릴리스”(예: “알파”, “베타”, “rc”)를 위한 특별한 의미의 버전이 필요하며, 이러한 버전에는 널리 쓰이는 별칭이 있습니다(“a”는 “알파”, “b”는 “베타”, “c”는 “rc”를 나타냅니다). 그리고 이러한 사전 릴리스 버전 때문에 버전 문자열 구성 요소를 단순한 영숫자 순서로 정렬할 수 없습니다. (예: 3.1a1 < 3.1)
- 일부 프로젝트에는 정규 버전의 “사후 릴리스”도 필요한데, 주로 다른 방법으로는 명확하게 표현할 수 없는 설치 프로그램 작업을 위해서입니다.
- 개발 버전을 사용하면 릴리스되지 않은 작업을 패키징하는 사람이 이후의 정규 릴리스와 버전이 충돌하는 것을 피할 수 있습니다.
한 걸음 더 나아가 도구를 사용하여 버전 번호를 관리하려는 사람들을 위한 주요 도구는 다음 두 가지입니다:
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가 버전을 비교하고 정렬하는 메커니즘을 분명히 제공하기는 하지만, 코드를 돌려 보지 않고도 사람이 그 정렬 순서를 합리적으로 짐작할 수 있는 버전 관리 명세라면 훨씬 더 바람직합니다.
또한 “major” 버전 번호에 날짜를 사용하면(예: 버전 문자열 “20090421”) RPM에서 문제가 생깁니다. 이렇게 하면 더 일반적인 “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 마커보다 앞에 정렬됩니다. 이를 사용해 사후 릴리스의 개발 버전을 만들 수 있습니다.
사전 릴리스에는 “알파”를 나타내는 a, “베타”를 나타내는 b, “릴리스 후보”를 나타내는 c를 사용할 수 있습니다. rc는 “릴리스 후보”의 대체 표기법으로, 버전 체계를 Python 자체의 버전 체계와 호환되게 하려고 추가되었습니다. rc는 c 뒤에 정렬됩니다:
>>> 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가 선호되는 마커라는 점에 유의하십시오.
verlib은 NormalizedVersion 클래스와 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의 많은 분들.
Copyright
This document has been placed in the public domain.