CPN 한국어 자습서 · 외부 문서 한국어 미러
UV 문서 · Concepts
Resolution · 원문: docs.astral.sh/uv/concepts/resolution/
아래는 원문을 한국어로 옮긴 미러입니다. 코드·명령은 원문 그대로이며, 가장 최신 정보는 하단 원문 링크에서 확인하세요.
해석(resolution)은 요구 사항 목록을 받아 해당 요구 사항을 충족하는 패키지 버전 목록으로 변환하는 과정입니다. 해석은 패키지의 호환 가능한 버전을 재귀적으로 검색하여 요청된 요구 사항이 충족되고 요청된 패키지의 요구 사항이 호환되는지 확인해야 합니다.
대부분의 프로젝트와 패키지에는 의존성(dependency)이 있습니다. 의존성은 현재 패키지가 작동하는 데 필요한 다른 패키지입니다. 패키지는 의존성을 요구 사항_으로 정의하는데, 대략 패키지 이름과 허용 가능한 버전의 조합입니다. 현재 프로젝트에서 정의한 의존성을 _직접 의존성_이라고 합니다. 현재 프로젝트의 각 의존성이 추가하는 의존성을 _간접 또는 _전이적 의존성_이라고 합니다.
해석 과정을 설명하기 위해 다음 의존성을 고려합니다:
foo와 bar에 의존합니다.foo에는 버전 1.0.0 하나만 있습니다:foo 1.0.0은 lib>=1.0.0에 의존합니다.bar에는 버전 1.0.0 하나만 있습니다:bar 1.0.0은 lib>=2.0.0에 의존합니다.lib에는 1.0.0과 2.0.0 두 가지 버전이 있습니다. 두 버전 모두 의존성이 없습니다.이 예시에서 리졸버(resolver)는 프로젝트 요구 사항을 충족하는 패키지 버전 집합을 찾아야 합니다. foo와 bar 모두 버전이 하나뿐이므로 해당 버전이 사용됩니다. 해석에는 전이적 의존성도 포함되어야 하므로 lib 버전을 선택해야 합니다. foo 1.0.0은 lib의 모든 사용 가능한 버전을 허용하지만 bar 1.0.0은 lib>=2.0.0을 요구하므로 lib 2.0.0을 사용해야 합니다.
일부 해석에서는 유효한 해결책이 여러 개 있을 수 있습니다. 다음 의존성을 고려합니다:
foo와 bar에 의존합니다.foo에는 1.0.0과 2.0.0 두 가지 버전이 있습니다:foo 1.0.0에는 의존성이 없습니다.foo 2.0.0은 lib==2.0.0에 의존합니다.bar에는 1.0.0과 2.0.0 두 가지 버전이 있습니다:bar 1.0.0에는 의존성이 없습니다.bar 2.0.0은 lib==1.0.0에 의존합니다.lib에는 1.0.0과 2.0.0 두 가지 버전이 있습니다. 두 버전 모두 의존성이 없습니다.이 예시에서 foo와 bar 모두 어떤 버전이든 선택해야 합니다. 그러나 어떤 버전을 선택할지는 각 버전의 의존성을 고려해야 합니다. foo 2.0.0과 bar 2.0.0은 lib의 요구 버전이 충돌하므로 함께 설치할 수 없습니다. 따라서 리졸버는 foo 1.0.0(bar 2.0.0과 함께) 또는 bar 1.0.0(foo 2.0.0과 함께)을 선택해야 합니다. 두 가지 모두 유효한 해결책이며, 서로 다른 해석 알고리즘이 어느 한 결과를 산출할 수 있습니다.
마커(marker)는 의존성을 언제 사용해야 하는지를 나타내는 표현식을 요구 사항에 첨부할 수 있게 합니다. 예를 들어 bar ; python_version < "3.9"는 bar가 Python 3.8 이하에서만 설치되어야 함을 나타냅니다.
마커는 현재 환경이나 플랫폼에 따라 패키지의 의존성을 조정하는 데 사용됩니다. 예를 들어 운영 체제, CPU 아키텍처, Python 버전, Python 구현체 등에 따라 의존성을 수정할 수 있습니다.
마커는 그 값이 필요한 의존성을 변경하기 때문에 해석에 중요합니다. 일반적으로 Python 패키지 리졸버는 패키지가 현재 플랫폼에 설치_되는 경우가 많기 때문에 _현재 플랫폼의 마커를 사용하여 어떤 의존성을 사용할지 결정합니다. 그러나 의존성을 _잠금(locking)_하는 경우에는 문제가 됩니다. 락파일(lockfile)이 락파일을 만든 것과 동일한 플랫폼을 사용하는 개발자에게만 작동하기 때문입니다. 이 문제를 해결하기 위해 플랫폼 독립적인 "보편적" 리졸버가 존재합니다.
uv는 플랫폼별 해석과 보편적 해석 모두를 지원합니다.
기본적으로 uv의 pip 인터페이스, 즉 uv pip compile은 pip-tools처럼 플랫폼별 해석을 생성합니다. uv의 프로젝트 인터페이스에서는 플랫폼별 해석을 사용할 방법이 없습니다.
uv는 --python-platform 및 --python-version 옵션을 사용하여 특정 대체 플랫폼과 Python 버전에 대한 해석도 지원합니다. 예를 들어, macOS에서 Python 3.12를 사용하는 경우 uv pip compile --python-platform linux --python-version 3.10 requirements.in을 사용하여 Linux의 Python 3.10에 대한 해석을 생성할 수 있습니다. 보편적 해석과 달리, 플랫폼별 해석에서 제공된 --python-version은 하한값이 아닌 정확한 Python 버전입니다.
Python의 환경 마커는 단순한 --python-platform 인수로 표현할 수 있는 것보다 훨씬 더 많은 현재 머신 정보를 노출합니다. 예를 들어, macOS의 platform_version 마커는 커널이 빌드된 시간을 포함하는데, 이는 (이론적으로) 패키지 요구 사항에 인코딩될 수 있습니다. uv의 리졸버는 대상 --python-platform에서 실행되는 모든 머신과 호환되는 해석을 생성하기 위해 최선의 노력을 다합니다. 이는 대부분의 사용 사례에 충분하지만, 복잡한 패키지와 플랫폼 조합에서는 정확도가 떨어질 수 있습니다.
uv의 락파일(uv.lock)은 보편적 해석으로 생성되며 플랫폼 간에 이식 가능합니다. 이를 통해 운영 체제, 아키텍처, Python 버전에 관계없이 프로젝트에서 작업하는 모든 사람의 의존성이 잠깁니다. uv 락파일은 uv lock, uv sync, uv add와 같은 프로젝트 명령으로 생성 및 수정됩니다.
보편적 해석은 --universal 플래그를 사용하여 uv의 pip 인터페이스, 즉 uv pip compile에서도 사용할 수 있습니다. 결과 요구 사항 파일에는 각 의존성이 관련 플랫폼을 나타내는 마커가 포함됩니다.
보편적 해석 중에 패키지는 서로 다른 플랫폼에서 서로 다른 버전이 필요한 경우 다른 버전이나 URL로 여러 번 나열될 수 있습니다. 마커가 어떤 버전이 사용될지를 결정합니다. 보편적 해석은 모든 마커의 요구 사항을 고려해야 하므로 플랫폼별 해석보다 더 제약이 많은 경우가 많습니다.
보편적 해석 중에 모든 필수 패키지는 pyproject.toml에 선언된 requires-python의 전체 범위와 호환되어야 합니다. 예를 들어, 프로젝트의 requires-python이 >=3.8인 경우, 주어진 의존성의 모든 버전이 Python 3.9 이상을 요구한다면 해석이 실패합니다. 이는 해당 의존성에 프로젝트의 지원 범위 하한인 Python 3.8에서 사용 가능한 버전이 없기 때문입니다. 즉, 프로젝트의 requires-python은 모든 의존성의 requires-python의 부분집합이어야 합니다.
주어진 의존성의 호환 버전을 선택할 때, uv는 기본적으로 지원되는 각 Python 버전에 대해 최신 호환 버전을 선택하려고 시도합니다. 예를 들어, 프로젝트의 requires-python이 >=3.8이고 의존성의 최신 버전이 Python 3.9 이상을 요구하지만 이전 버전은 모두 Python 3.8을 지원하는 경우, 리졸버는 Python 3.9 이상을 실행하는 사용자에게는 최신 버전을, Python 3.8을 실행하는 사용자에게는 이전 버전을 선택합니다.
의존성의 requires-python 범위를 평가할 때, uv는 하한만 고려하고 상한은 완전히 무시합니다. 예를 들어, >=3.8, <4는 >=3.8로 취급됩니다. requires-python의 상한을 준수하면 형식적으로는 올바르지만 실제로는 잘못된 해석이 발생하는 경우가 많습니다. 리졸버가 상한을 생략한 첫 번째 게시 버전으로 역추적하는 경우가 있기 때문입니다.
기본적으로 보편적 리졸버는 모든 플랫폼과 Python 버전에 대해 해석을 시도합니다.
프로젝트가 제한된 플랫폼이나 Python 버전 집합만 지원하는 경우, PEP 508 환경 마커 목록을 허용하는 environments 설정을 통해 해석할 플랫폼 집합을 제한할 수 있습니다. 즉, environments 설정을 사용하여 지원되는 플랫폼 집합을 _축소_할 수 있습니다.
예를 들어, 락파일을 macOS와 Linux로 제한하고 Windows에 대한 해석을 피하려면:
[tool.uv]
environments = [
"sys_platform == 'darwin'",
"sys_platform == 'linux'",
]
또는 대체 Python 구현체에 대한 해석을 피하려면:
[tool.uv]
environments = [
"implementation_name == 'cpython'"
]
environments 설정의 항목은 서로 겹치지 않아야 합니다(즉, 상호 배타적이어야 합니다). 예를 들어, sys_platform == 'darwin'과 sys_platform == 'linux'는 서로 배타적이지만, sys_platform == 'darwin'과 python_version >= '3.9'는 두 조건이 동시에 참일 수 있으므로 배타적이지 않습니다.
Python 생태계에서 패키지는 소스 배포판, 빌드된 배포판(휠), 또는 둘 다로 게시될 수 있습니다. 그러나 패키지를 설치하려면 빌드된 배포판이 필요합니다. 패키지에 빌드된 배포판이 없거나 현재 플랫폼이나 Python 버전에 대한 배포판이 없는 경우(빌드된 배포판은 종종 플랫폼별로 다름), uv는 소스에서 패키지를 빌드한 다음 결과 빌드된 배포판을 설치하려고 시도합니다.
일부 패키지(예: PyTorch)는 빌드된 배포판을 게시하지만 소스 배포판은 생략합니다. 이러한 패키지는 빌드된 배포판을 사용할 수 있는 플랫폼에서만 설치할 수 있습니다. 예를 들어, 패키지가 Linux용 빌드된 배포판을 게시하지만 macOS 또는 Windows용은 게시하지 않는 경우, 해당 패키지는 Linux에서만 설치할 수 있습니다.
소스 배포판이 없는 패키지는 보편적 해석에 문제를 일으킵니다. 일반적으로 패키지를 설치할 수 없는 플랫폼이나 Python 버전이 하나 이상 있기 때문입니다.
기본적으로 uv는 이러한 각 패키지에 대해 대상 Python 버전과 호환되는 하나 이상의 휠이 포함되어 있어야 합니다. required-environments 설정은 결과 해석에 특정 플랫폼용 휠이 포함되어 있는지 확인하거나, 해당 휠을 사용할 수 없는 경우 실패하도록 할 때 사용합니다. 이 설정은 PEP 508 환경 마커 목록을 허용합니다.
environments 설정이 uv가 의존성 해석 시 고려할 환경 집합을 제한_하는 반면, required-environments는 uv가 의존성 해석 시 _반드시 지원해야 하는 플랫폼 집합을 _확장_합니다.
예를 들어, environments = ["sys_platform == 'darwin'"]는 uv를 macOS에 대한 해석으로 제한합니다(Linux와 Windows 무시). 반면에 required-environments = ["sys_platform == 'darwin'"]는 소스 배포판이 없는 패키지가 설치 가능하려면 macOS용 휠을 포함해야 한다고 _요구_합니다(해당 휠을 사용할 수 없으면 실패합니다).
실제로 required-environments는 최신이 아닌 플랫폼에 대한 명시적 지원을 선언할 때 유용합니다. 이는 종종 해당 패키지의 최신 게시 버전을 넘어 역추적이 필요하기 때문입니다. 예를 들어, 빌드된 배포판 전용 패키지가 Intel macOS 지원을 포함하도록 보장하려면:
[tool.uv]
required-environments = [
"sys_platform == 'darwin' and platform_machine == 'x86_64'"
]
environments 및 required-environments 설정은 PEP 508 환경 마커를 허용합니다. 이 마커의 값은 Python 런타임(예: sys.platform, platform.machine(), platform.system(), os.name)에서 파생됩니다.
빠른 참고를 위해 플랫폼별로 가장 일반적인 마커 값은 다음과 같습니다:
| 마커 | Linux | macOS | Windows |
|---|---|---|---|
sys_platform |
'linux' |
'darwin' |
'win32' |
platform_system |
'Linux' |
'Darwin' |
'Windows' |
platform_machine (x86-64) |
'x86_64' |
'x86_64' |
'AMD64' |
platform_machine (ARM64) |
'aarch64' |
'arm64' |
'ARM64' |
os_name |
'posix' |
'posix' |
'nt' |
현재 플랫폼의 값을 확인하려면 다음을 실행합니다:
$ uvx python -c "import sysconfig; print(sysconfig.get_config_vars())"
해석 출력 파일, 즉 uv 락파일(uv.lock) 또는 요구 사항 출력 파일(requirements.txt)이 존재하면, 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 파일이 있다고 가정합니다:
flask>=2.0.0
uv pip compile requirements.in -o requirements.txt를 실행하면 다음 requirements.txt 파일이 생성됩니다:
# This file was autogenerated by uv via the following command:
# uv pip compile requirements.in -o requirements.txt
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 -o requirements.txt를 실행하면 대신 다음이 생성됩니다:
# This file was autogenerated by uv via the following command:
# uv pip compile --resolution lowest requirements.in -o requirements.txt
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
라이브러리를 게시할 때는 선언된 하한과의 호환성을 보장하기 위해 지속적 통합에서 --resolution lowest 또는 --resolution lowest-direct로 테스트를 별도로 실행하는 것이 좋습니다.
기본적으로 uv는 두 가지 경우에 의존성 해석 중 프리릴리스 버전을 허용합니다:
flask>=2.0.0rc1).전이적 프리릴리스로 인해 의존성 해석이 실패하면 uv는 모든 의존성에 대해 프리릴리스를 허용하는 --prerelease allow 사용을 안내합니다.
또는 전이적 의존성을 제약 조건이나 직접 의존성(즉, requirements.in 또는 pyproject.toml에)으로 프리릴리스 버전 지정자(예: flask>=2.0.0rc1)와 함께 추가하여 해당 특정 의존성에 대한 프리릴리스 지원을 선택할 수 있습니다.
프리릴리스는 모델링하기 까다롭고 다른 패키징 도구에서 버그의 빈번한 원인입니다. uv의 프리릴리스 처리는 의도적으로 제한적이며 정확성을 보장하기 위해 사용자가 프리릴리스에 명시적으로 동의해야 합니다.
보편적 해석 중에 패키지는 서로 다른 플랫폼이나 Python 버전에 다른 버전이 필요할 수 있으므로, 동일한 락파일 내에서 다른 버전이나 URL로 여러 번 나열될 수 있습니다.
--fork-strategy 설정을 사용하면 (1) 선택된 버전 수 최소화와 (2) 각 플랫폼에서 가능한 최신 버전 선택 사이에서 uv가 트레이드오프를 어떻게 조율할지 제어할 수 있습니다. 전자는 플랫폼 간 일관성을 높이고, 후자는 가능한 경우 더 새로운 패키지 버전을 사용하게 합니다.
기본값(--fork-strategy requires-python)에서 uv는 플랫폼 전반에서 선택된 버전 수를 최소화하면서 지원되는 각 Python 버전에 대해 각 패키지의 최신 버전을 선택하도록 최적화합니다.
예를 들어, Python 요구 사항이 >=3.8인 numpy를 해석할 때 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는 이 시나리오에 대한 완벽히 유효한 락파일을 구성할 수 있지만, 그렇게 하려면 패키지를 빌드해야 하는데 이는 Linux가 아닌 플랫폼에서 실패합니다.
tool.uv.dependency-metadata 테이블을 사용하여 이러한 의존성의 정적 메타데이터를 미리 제공할 수 있으며, 이를 통해 uv가 빌드 단계를 건너뛰고 제공된 메타데이터를 대신 사용할 수 있습니다.
예를 들어, chumpy에 대한 메타데이터를 미리 제공하려면 pyproject.toml에 dependency-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은 정적 메타데이터를 선언하지 않았습니다. flash-attn에 대한 메타데이터를 미리 선언하면 uv가 소스에서 패키지를 빌드하지 않고 flash-attn을 해석할 수 있습니다(이 자체는 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는 패키지의 메타데이터가 잘못되거나 불완전한 경우, 또는 패키지를 패키지 인덱스에서 사용할 수 없는 경우에도 사용할 수 있습니다. 의존성 재정의는 패키지의 허용된 버전을 전역적으로 재정의할 수 있는 반면, 메타데이터 재정의는 _특정 패키지_의 선언된 메타데이터를 재정의할 수 있습니다.
tool.uv.dependency-metadata의 version 필드는 레지스트리 기반 의존성의 경우 선택 사항입니다(생략하면 uv는 메타데이터가 해당 패키지의 모든 버전에 적용된다고 가정합니다). 그러나 직접 URL 의존성(Git 의존성 등)의 경우에는 _필수_입니다.
tool.uv.dependency-metadata 테이블의 항목은 Metadata 2.3 사양을 따르지만, uv가 읽는 것은 name, version, requires-dist, requires-python, provides-extra뿐입니다. version 필드도 선택 사항으로 간주됩니다. 생략하면 지정된 패키지의 모든 버전에 메타데이터가 사용됩니다.
uv는 프로젝트에서 선언된 모든 의존성이 서로 호환되어야 하며, 락파일을 생성할 때 모든 의존성을 함께 해석합니다. 여기에는 프로젝트 의존성, 선택적 의존성("extras"), 의존성 그룹(개발 의존성)이 포함됩니다.
한 extra에서 선언된 의존성이 다른 extra의 의존성과 호환되지 않으면 uv는 오류와 함께 프로젝트의 요구 사항 해석에 실패합니다. 예를 들어, 서로 충돌하는 두 가지 선택적 의존성 세트를 고려합니다:
[project.optional-dependencies] extra1 = ["numpy==2.1.2"] extra2 = ["numpy==2.0.0"]
위 의존성으로 uv lock을 실행하면 해석이 실패합니다:
$ uv lock
x No solution found when resolving dependencies:
`-> Because myproject[extra2] depends on numpy==2.0.0 and myproject[extra1] depends on numpy==2.1.2, we can conclude that myproject[extra1] and
myproject[extra2] are incompatible.
And because your project requires myproject[extra1] and myproject[extra2], we can conclude that your projects's requirements are unsatisfiable.
이를 해결하기 위해 uv는 충돌에 대한 명시적 선언을 지원합니다. extra1과 extra2가 충돌한다고 지정하면 uv는 이들을 별도로 해석합니다. tool.uv 섹션에서 충돌을 지정합니다:
[tool.uv]
conflicts = [
[
{ extra = "extra1" },
{ extra = "extra2" },
],
]
이제 uv lock 실행이 성공합니다. 그러나 이제 extra1과 extra2를 동시에 설치할 수 없습니다:
$ uv sync --extra extra1 --extra extra2
Resolved 3 packages in 14ms
error: extra `extra1`, extra `extra2` are incompatible with the declared conflicts: {`myproject[extra1]`, `myproject[extra2]`}
이 오류는 extra1과 extra2를 모두 설치하면 동일한 환경에 패키지의 두 가지 다른 버전이 설치되기 때문에 발생합니다.
충돌하는 선택적 의존성을 처리하는 위의 전략은 의존성 그룹에도 적용됩니다:
[dependency-groups]
group1 = ["numpy==2.1.2"]
group2 = ["numpy==2.0.0"]
[tool.uv]
conflicts = [
[
{ group = "group1" },
{ group = "group2" },
],
]
충돌하는 extras와의 유일한 차이점은 extra 키 대신 group 키를 사용해야 한다는 것입니다.
여러 프로젝트가 있는 워크스페이스(workspace)를 사용할 때도 동일한 제한이 적용됩니다. uv는 모든 워크스페이스 멤버가 서로 호환되어야 합니다. 마찬가지로, 워크스페이스 멤버 전반에서 충돌을 선언할 수 있습니다.
예를 들어, 다음 워크스페이스를 고려합니다:
member1/pyproject.toml
[project] name = "member1" [project.optional-dependencies] extra1 = ["numpy==2.1.2"]
member2/pyproject.toml
[project] name = "member2" [project.optional-dependencies] extra2 = ["numpy==2.0.0"]
이러한 서로 다른 워크스페이스 멤버의 extras 간 충돌을 선언하려면 package 키를 사용합니다:
[tool.uv]
conflicts = [
[
{ package = "member1", extra = "extra1" },
{ package = "member2", extra = "extra2" },
],
]
한 워크스페이스 멤버의 프로젝트 의존성(project.dependencies)이 다른 멤버의 extra와 충돌할 수도 있습니다. 예를 들어:
member1/pyproject.toml
[project] name = "member1" dependencies = ["numpy==2.1.2"]
member2/pyproject.toml
[project] name = "member2" [project.optional-dependencies] extra2 = ["numpy==2.0.0"]
이 충돌도 package 키를 사용하여 선언할 수 있습니다:
[tool.uv]
conflicts = [
[
{ package = "member1" },
{ package = "member2", extra = "extra2" },
],
]
마찬가지로, 일부 워크스페이스 멤버가 프로젝트 의존성이 충돌하는 경우도 있을 수 있습니다:
이 충돌도 package 키를 사용하여 선언할 수 있습니다:
[tool.uv]
conflicts = [
[
{ package = "member1" },
{ package = "member2" },
],
]
이러한 워크스페이스 멤버는 함께 설치할 수 없습니다. 예를 들어, 워크스페이스 루트는 다음을 정의할 수 없습니다:
[project] name = "root" dependencies = ["member1", "member2"]
기본적으로 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 필드를 지원해야 합니다. 주어진 배포판에 이 필드가 없으면 --exclude-newer-package <package>=false로 패키지를 선택 해제하거나, 인덱스 자체 exclude-newer 값으로 설정되거나, [[tool.uv.index]] exclude-newer = false로 인덱스를 선택 해제하지 않는 한 해당 배포판은 사용 불가로 취급됩니다. PyPI는 모든 패키지에 대해 upload-time을 제공합니다.
재현성을 보장하기 위해, 충족 불가능한 해석에 대한 메시지는 --exclude-newer 플래그로 인해 배포판이 제외되었다고 언급하지 않습니다. 더 새로운 배포판은 존재하지 않는 것처럼 취급됩니다.
--exclude-newer 옵션은 레지스트리에서 읽은 패키지에만 적용됩니다(Git 의존성 등과 달리). 또한 uv pip 인터페이스를 사용할 때 --reinstall 플래그가 제공되지 않는 한 uv는 이전에 설치된 패키지를 다운그레이드하지 않습니다. --reinstall 플래그가 제공되면 uv는 새로운 해석을 수행합니다.
이 옵션은 pyproject.toml에서도 지원됩니다:
[tool.uv] exclude-newer = "2006-12-02T02:07:43Z"
낮은 우선순위 설정 소스에서 전역 컷오프를 비활성화하려면 --exclude-newer false를 전달하거나, UV_EXCLUDE_NEWER=false를 설정하거나, 높은 우선순위 설정 파일에서 exclude-newer = false를 설정합니다.
영구 설정에서 지정할 때 로컬 날짜/시간은 허용되지 않습니다.
특정 패키지에 대한 값도 지정할 수 있습니다. 예를 들어 --exclude-newer-package setuptools=2006-12-02, 또는:
[tool.uv]
exclude-newer-package = { setuptools = "2006-12-02T02:07:43Z" }
패키지 옵션은 패키지를 제한에서 선택 해제하는 <package>=false도 허용합니다. 예를 들어 --exclude-newer-package setuptools=false, 또는:
[tool.uv]
exclude-newer-package = { setuptools = false }
이는 패키지의 더 새로운 버전을 일시적으로 사용하거나 업로드 시간을 게시하지 않는 인덱스에서 패키지를 해석할 수 있게 하는 데 유용합니다.
패키지별 값은 전역 및 인덱스별 값보다 우선합니다.
마찬가지로, 개별 인덱스는 전역 컷오프를 재정의할 수 있습니다:
[tool.uv] exclude-newer = "2006-12-02T02:07:43Z" [[tool.uv.index]] name = "internal" url = "https://internal.example.com/simple" exclude-newer = "7 days"
또는 해당 인덱스에 대해 완전히 비활성화할 수 있습니다:
[[tool.uv.index]] name = "internal" url = "https://internal.example.com/simple" exclude-newer = false
이는 upload-time을 게시하지 않는 프라이빗 인덱스에 유용하거나, 전역 동작을 유지하면서 특정 인덱스에 다른 재현성 창을 적용하는 데 유용합니다.
uv는 또한 지정된 기간보다 최신인 패키지를 무시하는 의존성 "쿨다운"을 지원합니다. 이는 커뮤니티가 새로운 버전의 패키지를 검토할 기회를 갖기까지 패키지 업데이트를 지연시킴으로써 보안 태세를 개선하는 좋은 방법입니다.
이 기능은 exclude-newer 옵션을 통해 사용 가능하며 동일한 의미론을 공유합니다.
절대값 대신 기간을 지정하여 의존성 쿨다운을 정의합니다. "친숙한" 기간(예: 24 hours, 1 week, 30 days) 또는 ISO 8601 기간(예: PT24H, P7D, P30D)을 사용할 수 있습니다.
기간은 로컬 시간대 의미론을 준수하지 않으며, 하루가 24시간이라고 가정하여 항상 고정된 초 수로 해석됩니다(예: DST 전환은 무시됩니다). 달과 년과 같은 달력 단위는 본질적으로 일관성 없는 길이이므로 허용되지 않습니다.
해석에 기간을 사용하면 현재 시간을 기준으로 타임스탬프가 계산됩니다. uv.lock 파일을 사용할 때 타임스탬프는 락파일에 포함됩니다. uv는 현재 시간이 변경되어도 락파일을 업데이트하지 않습니다. 대신, --upgrade 또는 --refresh를 사용할 때와 같이 새로운 해석이 수행될 때 타임스탬프를 업데이트합니다.
이 옵션은 pyproject.toml에서도 지원됩니다:
[tool.uv] exclude-newer = "1 week"
특정 패키지에 대한 값도 지정할 수 있습니다:
[tool.uv]
exclude-newer = "1 week"
exclude-newer-package = { setuptools = "30 days" }
PEP 625는 패키지가 소스 배포판을 gzip 타볼(.tar.gz) 아카이브로 배포해야 한다고 지정합니다. 이 사양 이전에는 하위 호환성을 위해 지원해야 하는 다른 아카이브 형식도 허용되었습니다. uv는 다음 형식의 아카이브 읽기 및 추출을 지원합니다:
.tar.gz, .tgz).tar.bz2, .tbz).tar.xz, .txz).tar.zst).tar.lz).tar.lzma).zip).tar.gz 이외의 소스 배포판 확장자 사용은 Python 패키징 생태계 전반에서 널리 또는 일관되게 지원되지 않으므로 강력히 권장하지 않습니다.
.tar.gz 이외의 소스 배포판 확장자에 대한 지원은 더 이상 사용되지 않으며 향후 uv 릴리스에서 제거될 예정입니다.
uv.lock 파일은 버전이 지정된 스키마(schema)를 사용합니다. 스키마 버전은 락파일의 version 필드에 포함됩니다.
특정 버전의 uv는 동일한 스키마 버전의 락파일을 읽고 쓸 수 있지만, 더 큰 스키마 버전의 락파일은 거부합니다. 예를 들어, uv 버전이 스키마 v1을 지원하는 경우 uv lock은 스키마 v2가 있는 기존 락파일을 만나면 오류를 반환합니다.
스키마 업데이트가 하위 호환적이었다면 스키마 v2를 지원하는 uv 버전은 스키마 v1의 락파일을 읽을 수도 있습니다. 그러나 이는 보장되지 않으며, uv는 오래된 스키마 버전의 락파일을 만나면 오류와 함께 종료될 수 있습니다.
스키마 버전은 공개 API의 일부로 간주되므로 주요 변경 사항으로 마이너 릴리스에서만 증가합니다. 따라서 주어진 마이너 uv 릴리스 내의 모든 uv 패치 버전은 전체 락파일 호환성이 보장됩니다. 즉, 락파일은 마이너 릴리스 간에만 거부될 수 있습니다.
락파일의 revision 필드는 락파일에 대한 하위 호환 변경 사항을 추적하는 데 사용됩니다(예: 배포판에 새 필드 추가). revision의 변경은 이전 버전의 uv가 오류를 발생시키지 않습니다.
리졸버의 내부에 대한 자세한 내용은 리졸버 참조 문서를 참고하세요.
원문(영어): https://docs.astral.sh/uv/concepts/resolution/ · 본 문서는 학습용 한국어 번역이며 원본의 권리는 원저작자(Astral)에게 있습니다.