워크스페이스 활용하기¶
Cargo에서 영감을 받은 워크스페이스는 "함께 관리되는 하나 이상의 패키지, 즉 _워크스페이스 멤버_의 집합"이다.
워크스페이스는 대규모 코드베이스를 공통 의존성을 가진 여러 패키지로 나누어 구성한다. 예를 들어, FastAPI 기반 웹 애플리케이션과 함께 별도의 Python 패키지로 버전 관리되고 유지되는 일련의 라이브러리를 동일한 Git 저장소에서 관리할 수 있다.
워크스페이스에서 각 패키지는 자신만의 pyproject.toml
을 정의하지만, 워크스페이스는 단일 lockfile을 공유한다. 이를 통해 워크스페이스가 일관된 의존성 세트로 동작하도록 보장한다.
따라서 uv lock
은 전체 워크스페이스에 대해 한 번에 동작한다. 반면 uv run
과 uv sync
은 기본적으로 워크스페이스 루트에서 동작하지만, 두 명령 모두 --package
인자를 받아 특정 워크스페이스 멤버에 대해 명령을 실행할 수 있다. 이를 통해 워크스페이스 디렉터리 어디에서나 특정 멤버에 대한 작업을 수행할 수 있다.
시작하기¶
작업 공간을 생성하려면 pyproject.toml
파일에 tool.uv.workspace
테이블을 추가한다. 이렇게 하면 해당 패키지를 루트로 하는 작업 공간이 암시적으로 생성된다.
Tip
기존 패키지 내에서 uv init
을 실행하면 새로 생성된 멤버가 작업 공간에 자동으로 추가된다. 작업 공간 루트에 tool.uv.workspace
테이블이 없으면 새로 생성한다.
작업 공간을 정의할 때는 members
(필수)와 exclude
(선택) 키를 지정해야 한다. 이 키들은 작업 공간에 포함하거나 제외할 특정 디렉터리를 각각 지정하며, glob 패턴 리스트를 받는다:
[project]
name = "albatross"
version = "0.1.0"
requires-python = ">=3.12"
dependencies = ["bird-feeder", "tqdm>=4,<5"]
[tool.uv.sources]
bird-feeder = { workspace = true }
[tool.uv.workspace]
members = ["packages/*"]
exclude = ["packages/seeds"]
members
glob 패턴에 포함되고 exclude
glob 패턴에 제외되지 않은 모든 디렉터리는 pyproject.toml
파일을 포함해야 한다. 작업 공간 멤버는 애플리케이션 또는 라이브러리 중 하나일 수 있으며, 두 유형 모두 작업 공간에서 지원된다.
모든 작업 공간에는 루트가 필요하며, 이 루트도 작업 공간 멤버이다. 위 예제에서 albatross
는 작업 공간 루트이며, packages
디렉터리 아래의 모든 프로젝트가 작업 공간 멤버가 된다. 단, seeds
는 예외이다.
기본적으로 uv run
과 uv sync
는 작업 공간 루트에서 동작한다. 위 예제에서 uv run
과 uv run --package albatross
는 동일하며, uv run --package bird-feeder
는 bird-feeder
패키지에서 명령을 실행한다.
워크스페이스 소스¶
워크스페이스 내에서 멤버 간의 의존성은 tool.uv.sources
를 통해 관리된다. 예를 들어:
[project]
name = "albatross"
version = "0.1.0"
requires-python = ">=3.12"
dependencies = ["bird-feeder", "tqdm>=4,<5"]
[tool.uv.sources]
bird-feeder = { workspace = true }
[tool.uv.workspace]
members = ["packages/*"]
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
이 예제에서 albatross
프로젝트는 워크스페이스의 멤버인 bird-feeder
프로젝트에 의존한다. tool.uv.sources
테이블의 workspace = true
키-값 쌍은 bird-feeder
의존성이 PyPI나 다른 레지스트리에서 가져오는 대신 워크스페이스에서 제공되어야 함을 나타낸다.
Note
워크스페이스 멤버 간의 의존성은 편집 가능하다.
워크스페이스 루트의 tool.uv.sources
정의는 특정 멤버의 tool.uv.sources
에서 재정의하지 않는 한 모든 멤버에 적용된다. 예를 들어, 다음 pyproject.toml
을 보자:
[project]
name = "albatross"
version = "0.1.0"
requires-python = ">=3.12"
dependencies = ["bird-feeder", "tqdm>=4,<5"]
[tool.uv.sources]
bird-feeder = { workspace = true }
tqdm = { git = "https://github.com/tqdm/tqdm" }
[tool.uv.workspace]
members = ["packages/*"]
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
이 경우, 모든 워크스페이스 멤버는 기본적으로 GitHub에서 tqdm
을 설치한다. 단, 특정 멤버가 자체 tool.uv.sources
테이블에서 tqdm
항목을 재정의하지 않는 한 이 규칙이 적용된다.
워크스페이스 레이아웃¶
가장 일반적인 워크스페이스 레이아웃은 루트 프로젝트와 함께 여러 라이브러리가 있는 구조로 생각할 수 있다.
예를 들어, 위의 예제를 이어서 설명하면, 이 워크스페이스는 albatross
를 명시적인 루트로 두고, packages
디렉토리 안에 두 개의 라이브러리(bird-feeder
와 seeds
)가 있다:
albatross
├── packages
│ ├── bird-feeder
│ │ ├── pyproject.toml
│ │ └── src
│ │ └── bird_feeder
│ │ ├── __init__.py
│ │ └── foo.py
│ └── seeds
│ ├── pyproject.toml
│ └── src
│ └── seeds
│ ├── __init__.py
│ └── bar.py
├── pyproject.toml
├── README.md
├── uv.lock
└── src
└── albatross
└── main.py
seeds
는 pyproject.toml
에서 제외되었기 때문에, 이 워크스페이스는 총 두 개의 멤버를 가진다: albatross
(루트)와 bird-feeder
이다.
워크스페이스 사용 시기(와 사용하지 말아야 할 때)¶
워크스페이스는 단일 저장소 내에서 여러 개의 상호 연결된 패키지를 개발하는 과정을 편리하게 만든다. 코드베이스가 복잡해지면, 독립적인 의존성과 버전 제약을 가진 작고 합성 가능한 패키지로 분할하는 것이 유용할 수 있다.
워크스페이스는 관심사의 분리와 격리를 강화한다. 예를 들어, uv에서는 코어 라이브러리와 커맨드라인 인터페이스를 별도의 패키지로 분리해 CLI와 독립적으로 코어 라이브러리를 테스트할 수 있다. 반대의 경우도 마찬가지다.
워크스페이스의 일반적인 사용 사례로는 다음과 같은 경우가 있다:
- 확장 모듈(Rust, C++ 등)로 구현된 성능이 중요한 서브루틴을 가진 라이브러리
- 플러그인 시스템을 가진 라이브러리. 각 플러그인은 루트에 의존성을 가진 별도의 워크스페이스 패키지다.
그러나 워크스페이스는 구성원 간에 충돌하는 요구사항이 있거나, 각 구성원이 별도의 가상 환경을 원하는 경우에는 적합하지 않다. 이런 경우에는 경로 의존성을 사용하는 것이 더 나은 선택이다. 예를 들어, albatross
와 그 구성원들을 워크스페이스로 묶는 대신, 각 패키지를 독립적인 프로젝트로 정의하고 패키지 간 의존성을 tool.uv.sources
에서 경로 의존성으로 정의할 수 있다:
[project]
name = "albatross"
version = "0.1.0"
requires-python = ">=3.12"
dependencies = ["bird-feeder", "tqdm>=4,<5"]
[tool.uv.sources]
bird-feeder = { path = "packages/bird-feeder" }
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
이 접근 방식은 많은 동일한 이점을 제공하지만, 의존성 해결과 가상 환경 관리에 대해 더 세밀한 제어가 가능하다. 단점은 uv run --package
를 더 이상 사용할 수 없고, 대신 관련 패키지 디렉토리에서 명령을 실행해야 한다는 점이다.
마지막으로, uv의 워크스페이스는 전체 워크스페이스에 대해 단일 requires-python
을 강제하며, 모든 구성원의 requires-python
값의 교집합을 취한다. 워크스페이스의 다른 구성원이 지원하지 않는 Python 버전에서 특정 구성원을 테스트해야 한다면, uv pip
를 사용해 별도의 가상 환경에 해당 구성원을 설치해야 할 수도 있다.
Note
Python은 의존성 격리를 제공하지 않기 때문에, uv는 패키지가 선언된 의존성만 사용하도록 보장할 수 없다. 특히 워크스페이스의 경우, uv는 패키지가 다른 워크스페이스 구성원이 선언한 의존성을 임포트하지 않도록 보장할 수 없다.