Skip to content

해결(Resolution)

해결은 요구 사항 목록을 가져와 해당 요구 사항을 충족하는 패키지 버전 목록으로 변환하는 과정이다. 해결 과정에서는 호환 가능한 패키지 버전을 재귀적으로 탐색하며, 요청된 요구 사항이 충족되는지 확인하고, 요청된 패키지의 요구 사항이 서로 호환되는지 검토한다.

의존성

대부분의 프로젝트와 패키지는 의존성을 갖는다. 의존성은 현재 패키지가 동작하기 위해 필요한 다른 패키지를 의미한다. 패키지는 의존성을 요구사항_으로 정의하며, 이는 패키지 이름과 허용 가능한 버전의 조합으로 이루어진다. 현재 프로젝트가 정의하는 의존성을 _직접 의존성_이라고 부른다. 현재 프로젝트의 각 의존성이 추가하는 의존성은 _간접 의존성 또는 _전이 의존성_이라고 한다.

Note

의존성에 대한 자세한 내용은 Python 패키징 문서의 의존성 지정자 페이지를 참고한다.

기본 예제

의존성 해결 과정을 설명하기 위해 다음 의존성 관계를 살펴본다:

  • 프로젝트는 foobar에 의존한다.
  • foo는 1.0.0 버전 하나만 있다:
    • foo 1.0.0lib>=1.0.0에 의존한다.
  • bar는 1.0.0 버전 하나만 있다:
    • bar 1.0.0lib>=2.0.0에 의존한다.
  • lib는 1.0.0과 2.0.0 두 가지 버전이 있다. 두 버전 모두 의존성이 없다.

이 예제에서 해결자는 프로젝트 요구사항을 만족하는 패키지 버전 집합을 찾아야 한다. foobar 모두 하나의 버전만 있으므로 해당 버전을 사용한다. 해결 과정에는 전이적 의존성도 포함되므로 lib의 버전을 선택해야 한다. foo 1.0.0lib의 모든 버전을 허용하지만, bar 1.0.0lib>=2.0.0를 요구하므로 lib 2.0.0를 사용해야 한다.

어떤 해결 과정에서는 유효한 해결책이 여러 개일 수 있다. 다음 의존성 관계를 살펴본다:

  • 프로젝트는 foobar에 의존한다.
  • foo는 1.0.0과 2.0.0 두 가지 버전이 있다:
    • foo 1.0.0는 의존성이 없다.
    • foo 2.0.0lib==2.0.0에 의존한다.
  • bar는 1.0.0과 2.0.0 두 가지 버전이 있다:
    • bar 1.0.0는 의존성이 없다.
    • bar 2.0.0lib==1.0.0에 의존한다.
  • lib는 1.0.0과 2.0.0 두 가지 버전이 있다. 두 버전 모두 의존성이 없다.

이 예제에서는 foobar의 버전을 선택해야 하지만, 각 버전의 의존성을 고려해야 한다. foo 2.0.0bar 2.0.0lib의 버전 요구사항이 충돌하므로 함께 설치할 수 없다. 따라서 해결자는 foo 1.0.0bar 2.0.0를 선택하거나 bar 1.0.0foo 1.0.0를 선택해야 한다. 두 가지 모두 유효한 해결책이며, 다른 해결 알고리즘은 서로 다른 결과를 내놓을 수 있다.

플랫폼 마커

마커는 특정 조건에서만 의존성을 사용하도록 지정하는 표현식을 추가할 수 있다. 예를 들어 bar ; python_version < "3.9"bar가 Python 3.8 이하 버전에서만 설치되어야 함을 나타낸다.

마커는 현재 환경이나 플랫폼에 따라 패키지의 의존성을 조정하는 데 사용된다. 운영 체제, CPU 아키텍처, Python 버전, Python 구현 등 다양한 조건에 따라 의존성을 수정할 수 있다.

Note

마커에 대한 자세한 내용은 Python Packaging 문서의 환경 마커 섹션을 참고한다.

마커는 의존성 해결 과정에서 중요한 역할을 한다. 마커의 값에 따라 필요한 의존성이 달라지기 때문이다. 일반적으로 Python 패키지 해결기는 현재 플랫폼의 마커를 사용해 어떤 의존성을 사용할지 결정한다. 이는 패키지가 현재 플랫폼에 설치되고 있기 때문이다. 하지만 잠금 의존성의 경우 이 방식은 문제가 된다. 잠금 파일이 생성된 플랫폼과 동일한 환경에서만 작동하기 때문이다. 이 문제를 해결하기 위해 플랫폼 독립적 또는 "범용" 해결기가 존재한다.

uv는 플랫폼별 해결범용 해결 모두를 지원한다.

플랫폼별 의존성 해결

기본적으로 uv의 pip 인터페이스, 즉 uv pip compilepip-tools와 마찬가지로 플랫폼별 의존성을 해결한다. 그러나 uv의 프로젝트 인터페이스에서는 플랫폼별 의존성 해결을 사용할 수 없다.

uv는 --python-platform--python-version 옵션을 통해 특정 플랫폼과 파이썬 버전에 대한 의존성을 해결할 수 있다. 예를 들어, macOS에서 파이썬 3.12를 사용 중이라면, uv pip compile --python-platform linux --python-version 3.10 requirements.in 명령어를 통해 리눅스 환경의 파이썬 3.10에 대한 의존성을 해결할 수 있다. 범용 의존성 해결과 달리, 플랫폼별 의존성 해결에서는 --python-version에 지정된 버전이 정확히 사용될 파이썬 버전이며, 최소 버전이 아니다.

참고

파이썬의 환경 마커는 단순한 --python-platform 인수로 표현할 수 있는 것보다 훨씬 더 많은 정보를 제공한다. 예를 들어, macOS의 platform_version 마커는 커널이 빌드된 시간을 포함하며, 이는 이론적으로 패키지 요구 사항에 반영될 수 있다. uv의 의존성 해결기는 대상 --python-platform에서 실행되는 모든 머신과 호환되는 해결을 생성하기 위해 최선을 다하지만, 복잡한 패키지와 플랫폼 조합에서는 정확도를 잃을 수 있다. 대부분의 사용 사례에서는 충분하지만, 특수한 경우에는 주의가 필요하다.

범용 해결 방식

uv의 lockfile(uv.lock)은 범용 해결 방식으로 생성되며, 플랫폼 간에 이식 가능하다. 이는 운영체제, 아키텍처, 파이썬 버전에 관계없이 프로젝트를 작업하는 모든 사람이 동일한 의존성을 사용할 수 있도록 보장한다. uv lockfile은 uv lock, uv sync, uv add와 같은 프로젝트 커맨드로 생성되고 수정된다.

범용 해결 방식은 uv의 pip 인터페이스인 uv pip compile에서도 사용할 수 있으며, --universal 플래그를 통해 활성화할 수 있다. 이렇게 생성된 요구사항 파일은 각 의존성이 어떤 플랫폼에 적합한지를 나타내는 마커를 포함한다.

범용 해결 방식에서는 서로 다른 플랫폼에 대해 다른 버전이나 URL이 필요한 경우, 패키지가 여러 번 나열될 수 있다. 마커는 어떤 버전이 사용될지를 결정한다. 범용 해결 방식은 모든 마커의 요구사항을 고려해야 하기 때문에 플랫폼별 해결 방식보다 더 제한적일 수 있다.

범용 해결 방식에서는 모든 필수 패키지가 pyproject.toml에 선언된 requires-python의 전체 범위와 호환되어야 한다. 예를 들어, 프로젝트의 requires-python>=3.8인 경우, 특정 의존성의 모든 버전이 파이썬 3.9 이상을 요구한다면 해결이 실패한다. 이는 프로젝트가 지원하는 범위의 하한(예: 파이썬 3.8)에서 사용 가능한 버전이 없기 때문이다. 즉, 프로젝트의 requires-python은 모든 의존성의 requires-python의 부분집합이어야 한다.

주어진 의존성에 대해 호환되는 버전을 선택할 때, uv는 (기본적으로) 지원되는 각 파이썬 버전에 대해 가장 최신의 호환 버전을 선택하려고 시도한다. 예를 들어, 프로젝트의 requires-python>=3.8이고, 특정 의존성의 최신 버전이 파이썬 3.9 이상을 요구하는 반면, 이전 버전들은 파이썬 3.8을 지원한다면, 해결자는 파이썬 3.9 이상을 실행하는 사용자에게는 최신 버전을, 파이썬 3.8을 실행하는 사용자에게는 이전 버전을 선택한다.

requires-python 범위를 평가할 때, uv는 하한만 고려하고 상한은 완전히 무시한다. 예를 들어, >=3.8, <4>=3.8로 처리된다. requires-python의 상한을 존중하면 형식적으로는 올바르지만 실제로는 잘못된 해결 결과가 나올 수 있다. 예를 들어, 해결자는 상한을 명시하지 않은 처음 발표된 버전으로 되돌아가는 경우가 발생할 수 있다(Requires-Python 상한 참조).

제한된 환경 해상도

기본적으로 유니버설 리졸버는 모든 플랫폼과 Python 버전을 대상으로 해결을 시도한다.

프로젝트가 특정 플랫폼이나 Python 버전만 지원한다면, environments 설정을 통해 해결 대상 플랫폼을 제한할 수 있다. environments 설정은 PEP 508 환경 마커 목록을 받는다. 즉, 이 설정을 사용해 지원 플랫폼 집합을 줄일 수 있다.

예를 들어, lockfile을 macOS와 Linux로 제한하고 Windows는 제외하려면 다음과 같이 설정한다:

pyproject.toml
[tool.uv]
environments = [
    "sys_platform == 'darwin'",
    "sys_platform == 'linux'",
]

또는, 다른 Python 구현체를 제외하려면:

pyproject.toml
[tool.uv]
environments = [
    "implementation_name == 'cpython'"
]

environments 설정의 항목들은 서로 겹치지 않아야 한다(즉, 중복되지 않아야 한다). 예를 들어, sys_platform == 'darwin'sys_platform == 'linux'는 겹치지 않지만, sys_platform == 'darwin'python_version >= '3.9'는 동시에 참일 수 있기 때문에 겹친다.

필요한 환경 설정

파이썬 생태계에서 패키지는 소스 배포판(source distribution), 빌드된 배포판(built distribution, wheels), 또는 둘 다로 공개된다. 하지만 패키지를 설치하려면 빌드된 배포판이 필요하다. 만약 패키지가 빌드된 배포판을 제공하지 않거나, 현재 플랫폼이나 파이썬 버전에 맞는 배포판이 없다면(빌드된 배포판은 보통 플랫폼에 특화되어 있다), uv는 소스로부터 패키지를 빌드한 후, 그 결과로 생성된 빌드된 배포판을 설치하려고 시도한다.

일부 패키지(예: PyTorch)는 빌드된 배포판을 제공하지만, 소스 배포판은 제공하지 않는다. 이런 패키지는 빌드된 배포판이 제공되는 플랫폼에서만 설치할 수 있다. 예를 들어, 어떤 패키지가 리눅스용 빌드된 배포판은 제공하지만, macOS나 윈도우용은 제공하지 않는다면, 그 패키지는 윈도우에서는 설치할 수 없다.

소스 배포판이 없는 패키지는 범용 해결(universal resolution)에 문제를 일으킨다. 왜냐하면 일반적으로 설치할 수 없는 플랫폼이나 파이썬 버전이 적어도 하나 이상 존재하기 때문이다.

기본적으로, uv는 각 패키지가 대상 파이썬 버전과 호환되는 최소한 하나의 휠(wheel)을 포함하도록 요구한다. required-environments 설정을 사용하면 특정 플랫폼에 대한 휠이 결과 해결에 포함되도록 보장하거나, 해당 휠이 없을 경우 실패하도록 할 수 있다.

environments 설정이 uv가 의존성을 해결할 때 고려할 환경을 제한한다면, required-environments 설정은 uv가 반드시 지원해야 하는 플랫폼의 범위를 확장한다.

예를 들어, environments = ["sys_platform == 'darwin'"]는 uv가 macOS만 고려하도록 제한한다(리눅스와 윈도우는 무시). 반면에, required-environments = ["sys_platform == 'darwin'"]는 소스 배포판이 없는 패키지가 macOS용 휠을 포함해야 설치 가능하도록 요구한다(해당 휠이 없으면 실패).

실제로 required-environments는 최신이 아닌 플랫폼에 대한 명시적 지원을 선언하는 데 유용하다. 왜냐하면 이는 종종 해당 패키지의 최신 버전을 지나서 역추적(backtracking)을 필요로 하기 때문이다. 예를 들어, 빌드된 배포판만 제공하는 패키지가 Intel macOS를 지원하도록 보장하려면 다음과 같이 설정할 수 있다:

pyproject.toml
[tool.uv]
required-environments = [
    "sys_platform == 'darwin' and platform_machine == 'x86_64'"
]

의존성 우선순위 설정

해결 결과 파일이 존재하는 경우, 즉 uv 잠금 파일(uv.lock)이나 요구사항 출력 파일(requirements.txt)이 있을 때, uv는 해당 파일에 나열된 의존성 버전을 우선적으로 사용한다. 마찬가지로, 가상 환경에 패키지를 설치할 때, uv는 이미 설치된 버전이 있다면 그것을 우선적으로 선택한다. 이는 잠긴 버전이나 설치된 버전이 호환되지 않는 버전이 요청되거나 --upgrade 옵션을 통해 명시적으로 업그레이드를 요청하지 않는 한 변경되지 않음을 의미한다.

의존성 해결 전략

기본적으로 uv는 각 패키지의 최신 버전을 사용하려고 한다. 예를 들어, uv pip install flask>=2.0.0 명령을 실행하면 Flask의 최신 버전(예: 3.0.0)이 설치된다. 만약 flask>=2.0.0이 프로젝트의 의존성으로 지정되어 있다면, flask 3.0.0만 사용된다. 이는 테스트를 실행할 때 프로젝트가 실제로 명시된 최소 버전인 flask 2.0.0과 호환되는지 확인하지 않기 때문에 중요하다.

--resolution lowest 옵션을 사용하면 uv는 모든 의존성(직접 및 간접)에 대해 가능한 가장 낮은 버전을 설치한다. 반면 --resolution lowest-direct 옵션은 모든 직접 의존성에 대해 가장 낮은 호환 버전을 사용하고, 나머지 의존성에는 최신 호환 버전을 사용한다. uv는 항상 빌드 의존성에 대해 최신 버전을 사용한다.

예를 들어, 다음과 같은 requirements.in 파일이 있다고 가정하자:

requirements.in
flask>=2.0.0

uv pip compile requirements.in 명령을 실행하면 다음과 같은 requirements.txt 파일이 생성된다:

requirements.txt
# 이 파일은 uv에 의해 자동 생성되었으며, 다음 명령으로 생성됨:
#    uv pip compile requirements.in
blinker==1.7.0
    # via flask
click==8.1.7
    # via flask
flask==3.0.0
itsdangerous==2.1.2
    # via flask
jinja2==3.1.2
    # via flask
markupsafe==2.1.3
    # via
    #   jinja2
    #   werkzeug
werkzeug==3.0.1
    # via flask

그러나 uv pip compile --resolution lowest requirements.in 명령을 실행하면 다음과 같은 결과가 나온다:

requirements.in
# 이 파일은 uv에 의해 자동 생성되었으며, 다음 명령으로 생성됨:
#    uv pip compile requirements.in --resolution lowest
click==7.1.2
    # via flask
flask==2.0.0
itsdangerous==2.0.0
    # via flask
jinja2==3.0.0
    # via flask
markupsafe==2.0.0
    # via jinja2
werkzeug==2.0.0
    # via flask

라이브러리를 배포할 때는 지속적 통합(CI)에서 --resolution lowest 또는 --resolution lowest-direct 옵션을 사용해 별도로 테스트를 실행하는 것이 좋다. 이를 통해 선언된 최소 버전과의 호환성을 보장할 수 있다.

프리릴리스 버전 처리

기본적으로 uv는 두 가지 경우에 의존성 해결 과정에서 프리릴리스 버전을 허용한다:

  1. 패키지가 직접 의존성이고, 버전 지정자가 프리릴리스 지정자를 포함하는 경우 (예: flask>=2.0.0rc1).
  2. 패키지의 모든 공개 버전이 프리릴리스인 경우.

의존성 해결이 전이적 프리릴리스로 인해 실패하면, uv는 --prerelease allow 사용을 권장해 모든 의존성에 대해 프리릴리스를 허용하도록 한다.

또는, 전이적 의존성을 제약 조건으로 추가하거나 직접 의존성으로 추가해 (예: requirements.in 또는 pyproject.toml) 특정 의존성에 대해 프리릴리스 지원을 선택할 수 있다 (예: flask>=2.0.0rc1).

프리릴리스 버전은 모델링하기 어렵기로 유명하며, 다른 패키징 도구에서 버그의 주요 원인이 된다. uv의 프리릴리스 처리는 의도적으로 제한적이며, 사용자가 프리릴리스를 명시적으로 선택해야 정확성을 보장한다.

자세한 내용은 프리릴리스 호환성을 참고한다.

다중 버전 해결

유니버설 해결 과정에서, 동일한 lockfile 내에서 서로 다른 플랫폼이나 Python 버전에 따라 다른 버전이나 URL이 필요할 수 있으므로, 패키지가 여러 번 나열될 수 있다.

--fork-strategy 설정은 uv가 (1) 선택된 버전 수를 최소화하는 것과 (2) 각 플랫폼에 대해 가능한 최신 버전을 선택하는 것 사이에서 어떻게 균형을 맞출지 제어한다. 전자는 플랫폼 간 일관성을 높이는 반면, 후자는 가능한 한 최신 패키지 버전을 사용하도록 한다.

기본값(--fork-strategy requires-python)으로, uv는 각 지원되는 Python 버전에 대해 가능한 최신 버전을 선택하면서도 플랫폼 간 선택된 버전 수를 최소화하도록 최적화한다.

예를 들어, numpy를 Python 버전 >=3.8 요구 사항으로 해결할 때, uv는 다음과 같은 버전을 선택한다:

numpy==1.24.4 ; python_version == "3.8"
numpy==2.0.2 ; python_version == "3.9"
numpy==2.2.0 ; python_version >= "3.10"

이 해결 방식은 NumPy 2.2.0 이상이 Python 3.10 이상을 필요로 하는 반면, 이전 버전은 Python 3.8과 3.9와 호환된다는 사실을 반영한다.

--fork-strategy fewest 설정에서는, uv는 각 패키지에 대해 선택된 버전 수를 최소화하며, 더 넓은 범위의 지원 Python 버전이나 플랫폼과 호환되는 이전 버전을 선호한다.

예를 들어, 위의 시나리오에서 uv는 Python 3.9에 대해 numpy==2.0.2, Python 3.10 이상에 대해 numpy==2.2.0으로 업그레이드하는 대신, 모든 Python 버전에 대해 numpy==1.24.4를 선택한다.

의존성 제약 조건

pip와 마찬가지로, uv는 특정 패키지에 대해 허용 가능한 버전의 범위를 좁히는 제약 조건 파일(--constraint constraints.txt)을 지원한다. 제약 조건 파일은 요구 사항 파일과 유사하지만, 단순히 제약 조건으로 나열된 경우 해당 패키지가 해결 과정에 포함되지는 않는다. 대신, 요청된 패키지가 직접적이거나 전이적 의존성으로 이미 포함된 경우에만 제약 조건이 적용된다. 제약 조건은 전이적 의존성에 대해 사용 가능한 버전의 범위를 줄이는 데 유용하다. 또한, 두 세트 간에 겹치는 패키지가 무엇인지에 관계없이 해결된 버전을 다른 세트와 동기화하는 데 사용할 수도 있다.

의존성 오버라이드

의존성 오버라이드는 패키지의 선언된 의존성을 덮어쓰는 방식으로, 실패하거나 원치 않는 해결 방식을 우회할 수 있게 한다. 오버라이드는 메타데이터가 다르게 표시하더라도 특정 버전의 패키지와 호환된다는 것을 알고 있을 때 유용한 최후의 수단이다.

예를 들어, 전이적 의존성이 pydantic>=1.0,<2.0을 요구하지만 실제로는 pydantic>=2.0과도 호환된다면, 사용자는 오버라이드에 pydantic>=1.0,<3을 포함시켜 선언된 의존성을 덮어쓸 수 있다. 이를 통해 리졸버가 더 새로운 버전의 pydantic을 선택할 수 있게 된다.

구체적으로, pydantic>=1.0,<3이 오버라이드로 포함되면, uv는 pydantic에 대한 모든 선언된 요구 사항을 무시하고 이를 오버라이드로 대체한다. 위의 예제에서 pydantic>=1.0,<2.0 요구 사항은 완전히 무시되고, 대신 pydantic>=1.0,<3으로 대체된다.

제약 조건은 패키지의 허용 가능한 버전 집합을 줄이는 데 그치지만, 오버라이드는 허용 가능한 버전 집합을 확장할 수 있다. 이를 통해 잘못된 상위 버전 경계를 우회할 수 있다. 제약 조건과 마찬가지로, 오버라이드는 패키지에 대한 의존성을 추가하지 않으며, 직접적이거나 전이적 의존성에서 패키지가 요청될 때만 효과를 발휘한다.

pyproject.toml에서는 tool.uv.override-dependencies를 사용해 오버라이드 목록을 정의한다. pip 호환 인터페이스에서는 --override 옵션을 사용해 제약 조건 파일과 동일한 형식의 파일을 전달할 수 있다.

동일한 패키지에 대해 여러 오버라이드가 제공되는 경우, 마커를 사용해 구분해야 한다. 패키지가 마커가 있는 의존성을 가지고 있다면, 오버라이드를 사용할 때 마커가 참이든 거짓이든 관계없이 무조건 대체된다.

의존성 메타데이터

uv는 패키지의 의존성을 확인하기 위해 해석 과정에서 각 패키지의 메타데이터를 해석한다. 이 메타데이터는 일반적으로 패키지 인덱스에 정적 파일로 제공된다. 하지만 소스 배포만 제공하는 패키지의 경우, 메타데이터가 미리 제공되지 않을 수 있다.

이런 경우, uv는 메타데이터를 확인하기 위해 패키지를 빌드해야 한다(예: setup.py 호출). 이로 인해 해석 과정에서 성능 저하가 발생할 수 있다. 또한 패키지가 모든 플랫폼에서 빌드 가능해야 하는 요구사항이 생기는데, 이는 항상 가능하지 않다.

예를 들어, 특정 패키지가 Linux에서만 빌드 및 설치되도록 설계되었을 수 있다. 이 경우 macOS나 Windows에서는 빌드가 실패할 것이다. uv는 이 시나리오에서 완벽히 유효한 lockfile을 생성할 수 있지만, 이를 위해 패키지를 빌드해야 하며, Linux가 아닌 플랫폼에서는 실패할 것이다.

tool.uv.dependency-metadata 테이블을 사용하면 이러한 의존성에 대한 메타데이터를 미리 제공할 수 있다. 이를 통해 uv는 빌드 단계를 건너뛰고 제공된 메타데이터를 사용할 수 있다.

예를 들어, chumpy에 대한 메타데이터를 미리 제공하려면 pyproject.tomldependency-metadata를 포함하면 된다:

[[tool.uv.dependency-metadata]]
name = "chumpy"
version = "0.70"
requires-dist = ["numpy>=1.8.1", "scipy>=0.13.0", "six>=1.11.0"]

이 선언은 패키지가 미리 정적 메타데이터를 선언하지 않은 경우를 위한 것이다. 또한 빌드 격리를 비활성화해야 하는 패키지에도 유용하다. 이런 경우, 패키지 해석 전 커스텀 빌드 환경을 만드는 대신 메타데이터를 미리 선언하는 것이 더 간단할 수 있다.

예를 들어, flash-attn에 대한 메타데이터를 선언하면 uv가 소스에서 패키지를 빌드하지 않고도 해석할 수 있다(torch 설치가 필요 없음):

[project]
name = "project"
version = "0.1.0"
requires-python = ">=3.12"
dependencies = ["flash-attn"]

[tool.uv.sources]
flash-attn = { git = "https://github.com/Dao-AILab/flash-attention", tag = "v2.6.3" }

[[tool.uv.dependency-metadata]]
name = "flash-attn"
version = "2.6.3"
requires-dist = ["torch", "einops"]

의존성 오버라이드와 마찬가지로, tool.uv.dependency-metadata는 패키지의 메타데이터가 잘못되었거나 불완전한 경우, 또는 패키지가 인덱스에 없는 경우에도 사용할 수 있다. 의존성 오버라이드는 패키지의 허용 버전을 전역적으로 재정의하는 반면, 메타데이터 오버라이드는 특정 패키지의 선언된 메타데이터를 재정의한다.

Note

tool.uv.dependency-metadataversion 필드는 레지스트리 기반 의존성의 경우 선택 사항이다(생략하면 uv는 해당 메타데이터가 패키지의 모든 버전에 적용된다고 가정한다). 하지만 Git 의존성과 같은 직접 URL 의존성의 경우 필수이다.

tool.uv.dependency-metadata 테이블의 항목은 Metadata 2.3 사양을 따르지만, uv는 name, version, requires-dist, requires-python, provides-extra만 읽는다. version 필드는 선택 사항으로 간주되며, 생략하면 해당 메타데이터는 지정된 패키지의 모든 버전에 적용된다.

하한 버전

기본적으로 uv add 명령은 의존성에 하한 버전을 추가한다. uv를 사용해 프로젝트를 관리할 때 직접적인 의존성이 하한 버전을 지정하지 않으면 경고를 표시한다.

하한 버전은 일반적인 상황에서는 크게 중요하지 않지만, 의존성 충돌이 발생할 때는 매우 중요하다. 예를 들어, 두 개의 패키지를 필요로 하는 프로젝트가 있고, 이 패키지들이 서로 충돌하는 의존성을 가지고 있다고 가정해보자. 리졸버는 두 패키지의 제약 조건 내에서 모든 버전 조합을 확인해야 한다. 만약 모든 조합이 충돌한다면, 의존성을 충족할 수 없기 때문에 오류가 발생한다. 하한 버전이 없다면, 리졸버는 종종 패키지의 가장 오래된 버전으로 되돌아갈 수 있다. 이는 단순히 속도가 느린 문제뿐만 아니라, 오래된 버전의 패키지는 빌드에 실패할 가능성이 높다. 또한 리졸버가 충돌하는 패키지에 의존하지 않을 정도로 오래된 버전을 선택할 수도 있지만, 이 버전은 여러분의 코드와 호환되지 않을 수 있다.

라이브러리를 작성할 때 하한 버전은 특히 중요하다. 라이브러리가 작동하는 각 의존성의 최소 버전을 명시하고, 이 하한 버전이 정확한지 검증하는 것이 필수적이다. 이를 위해 --resolution lowest 또는 --resolution lowest-direct 옵션을 사용해 테스트할 수 있다. 그렇지 않으면 사용자가 라이브러리의 의존성 중 오래되고 호환되지 않는 버전을 받게 될 수 있으며, 라이브러리가 예기치 않은 오류로 실패할 수 있다.

재현 가능한 의존성 해결

uv는 --exclude-newer 옵션을 지원하여 특정 날짜 이전에 출시된 배포판만을 대상으로 의존성을 해결한다. 이를 통해 새로운 패키지 릴리스와 상관없이 동일한 설치 환경을 재현할 수 있다. 날짜는 RFC 3339 형식의 타임스탬프(예: 2006-12-02T02:07:43Z) 또는 시스템에 설정된 시간대를 기준으로 동일한 형식의 로컬 날짜(예: 2006-12-02)로 지정할 수 있다.

이 기능을 사용하려면 패키지 인덱스가 PEP 700에서 정의한 upload-time 필드를 지원해야 한다. 특정 배포판에 이 필드가 없으면 해당 배포판은 사용할 수 없는 것으로 처리된다. PyPI는 모든 패키지에 대해 upload-time 정보를 제공한다.

재현성을 보장하기 위해, 해결이 불가능한 경우 --exclude-newer 플래그로 인해 배포판이 제외되었다는 메시지를 표시하지 않는다. 대신, 더 최신의 배포판은 존재하지 않는 것처럼 처리한다.

Note

--exclude-newer 옵션은 레지스트리에서 읽어온 패키지에만 적용된다(예: Git 의존성은 제외). 또한, uv pip 인터페이스를 사용할 때, --reinstall 플래그를 제공하지 않으면 uv는 이전에 설치된 패키지를 다운그레이드하지 않는다. --reinstall 플래그를 제공하면 uv는 새로운 의존성 해결을 수행한다.

소스 배포

PEP 625는 패키지가 소스 배포를 gzip tarball(.tar.gz) 아카이브 형태로 제공해야 한다고 명시한다. 이 명세 이전에는 하위 호환성을 위해 다른 아카이브 형식도 허용되었다. uv는 다음과 같은 형식의 아카이브를 읽고 추출할 수 있다:

  • gzip tarball (.tar.gz, .tgz)
  • bzip2 tarball (.tar.bz2, .tbz)
  • xz tarball (.tar.xz, .txz)
  • zstd tarball (.tar.zst)
  • lzip tarball (.tar.lz)
  • lzma tarball (.tar.lzma)
  • zip (.zip)

더 알아보기

리졸버의 내부 동작에 대해 더 자세히 알고 싶다면 리졸버 내부 구조 문서를 참고한다.

Lockfile 버전 관리

uv.lock 파일은 버전 관리 스키마를 사용한다. 스키마 버전은 lockfile의 version 필드에 포함된다.

특정 uv 버전은 동일한 스키마 버전의 lockfile을 읽고 쓸 수 있지만, 더 높은 스키마 버전의 lockfile은 거부한다. 예를 들어, uv 버전이 스키마 v1을 지원하는 경우, 스키마 v2가 포함된 기존 lockfile을 만나면 uv lock 명령어는 오류를 발생시킨다.

스키마 v2를 지원하는 uv 버전은 스키마 업데이트가 하위 호환성이 있는 경우 스키마 v1 lockfile을 읽을 수 있다. 하지만 이는 보장되지 않으며, uv는 오래된 스키마 버전의 lockfile을 만나면 오류와 함께 종료될 수 있다.

스키마 버전은 공개 API의 일부로 간주되므로, 주요 변경 사항으로 간주되어 마이너 릴리스에서만 버전이 상승한다(버전 관리 참조). 따라서 특정 uv 마이너 릴리스 내의 모든 패치 버전은 lockfile 호환성을 완전히 보장한다. 즉, lockfile은 마이너 릴리스 간에만 거부될 수 있다.