HAEJUN RECORDS

Summary

  • uv run -m src.db.csv_to_snowpg --옵션... 같은 긴 명령어를 uv run csv-to-snowpg처럼 짧게 만들 수 있다
  • 핵심은 [project.scripts]에 “이 이름을 부르면 이 함수를 실행해”라는 단축키를 등록하는 것이다
  • 단축키가 동작하려면 [build-system]이 있어야 한다. 그래야 uv가 내 프로젝트를 패키지로 포장해 .venv에 설치한다
  • uv_build의 module-root 기본값이 src라서 설정에도, import에도 src.를 쓰지 않는다
  • 파라미터는 코드 안의 기본값과 .env로 옮겨서 줄인다

들어가며

uv 프로젝트 관리에서 pyproject.toml을 “설계도”라고 정리했다. 이 설계도에는 필요한 재료(dependencies)만 적는 게 아니라 실행 버튼도 적을 수 있다.

예를 들어 터미널에서 Python 모듈을 실행할 때마다 src.package.module --param value처럼 모듈 경로 전체와 인자를 입력해 왔다고 하자. pyproject.toml에 entry point를 등록하면 이 입력을 짧은 명령어 하나로 줄일 수 있다.

이 글은 명령어 단축에 필요한 TOML 파일의 구성과 설정 방법, 그리고 단축이 동작하는 원리를 다룬다.

명령어 단축 전후 비교

# AS-IS
uv run -m src.package.module --param1 value1 --param2 value2
 
# TO-BE
uv run module

TOML 파일

개요

Pydantic과 TOML로 Config 관리에서 정리한 TOML의 장점은 다음과 같다.

  1. 도구 기능별 문단 구분 : [제목]으로 칸을 나눠 들여쓰기 문제로 깨지지 않음
  2. 간결한 값 선언 : 이름 = 값 형태로 직관적인 선언문
  3. Python 공식 표준 : uv, pytest, ruff 등 여러 도구가 모두 같은 파일을 읽음(PEP 518/621)
  4. Python 내장 모듈 : tomllib로 별도 설치 없이 바로 읽음

pyproject.toml은 [제목] 칸마다 담당하는 역할이 다르다. 이 글에서는 아래 예시의 세 칸만 다룬다.

  • [project] : 프로젝트의 개요(명칭, 버전, 설명 등)
  • [build-system] : 패키지 빌드 도구 정의
  • [tool] : 특정 도구 전용 칸

pyproject.toml 예시

[project]
name = "my-project"
version = "0.1.0"
description = "이 글은 프로젝트 설명글입니다."
readme = "README.md",
requires-python = ">=3.12"
dependencies = [
	"polars>=2.1.0", 
	"psycopg[binary]>=3.3.6", 
	"python-dotenv>=1.2.4"
]
 
[project.scripts]
csv_to_snowpg = "DataUpload.csv_to_snowpg:main"
 
[build-system]
requires = ["uv_build>=0.11.3,<0.12"]
build-backend = "uv_build"
 
[tool.uv.build-backend]
module-name = ["DataUpload"]
# module-root = "src"

위 예시의 폴더 구조는 다음과 같다.

예시 폴더 구조

my-project/
├── pyproject.toml
├── .env
└── src/
    └── DataUpload/
        ├── __init__.py
        └── csv_to_snowpg.py   ← main() 함수가 있음

project

[project]는 프로젝트의 기본 정보를 담고 있으며, [project.scripts]를 통해 단축키를 등록할 수 있다.

[project]에 들어가는 선언값은 다음과 같다.

명칭기능
name프로젝트 이름
version프로젝트 버전. 업데이트 명령어를 실행할 때 이 값을 가져올 수 있다
description프로젝트에 대한 설명글
readme프로젝트 개요에 대한 파일명. 대표적으로 README.md
requires-python요구하는 Python 버전
dependencies코드 실행에 필요한 패키지와 최소 버전. 패키지 간 종속성 충돌을 막는다

project.scripts는 단축키를 등록하기 위한 설정이다. 아래 예시는 “csv-to-snowpg라고 부르면 src/DataUpload/csv_to_snowpg.py의 main()을 실행해”라는 뜻이다. 이 설정이 없으면 예전과 같이 -m src.DataUpload.csv_to_snowpg처럼 긴 경로를 매번 입력해야 한다.

project.scripts 항목 읽는 법

  csv_to_snowpg = "DataUpload.csv_to_snowpg : main"
     ↑ 부를 이름              ↑ 어느 파일에서      ↑ 어떤 함수를 실행할지

uv sync를 하면 운영체제에 맞는 실행 파일이 생긴다. Windows는 .venv/Scripts/csv-to-snowpg.exe, macOS·Linux는 .venv/bin/csv-to-snowpg이다.

build-system

[build-system]은 프로젝트를 패키지로 빌드할 담당자를 지정하는 칸이다.

uv sync를 하면 uv는 내 프로젝트 자체를 하나의 패키지로 말아서 .venv에 넣는다. pandas, numpy 같은 패키지를 설치할 때와 같은 방식이며, 이 작업을 빌드(build)라고 한다.

build-backend 는 이 프로젝트를 누가 빌드할지, 즉 빌드 담당자를 지정하는 값이다. Python의 빌드 담당자(빌드 도구)에는 setuptools, hatchling, poetry-core 등이 있으며, uv 팀이 만든 uv_build가 대표적이다. 이 글은 uv_build를 다룬다.

requires 는 빌드를 수행하기 전에 설치할 빌드 담당자와 그 버전 조건이다. 쉽게 말해 빌드 전에 uv_build 0.11.3 이상, 0.12 미만을 먼저 설치해라는 뜻이다. <0.12로 상한을 둔 이유는 uv_build에 큰 업데이트가 나와도 패키징 방식이 바뀌지 않게 하기 위해서다.

requires와 dependencies는 필요한 시점과 .venv에 남는지가 다르다.

구분dependenciesrequires
필요한 시점코드가 실행될 때코드 실행 전, 패키징할 때만
.venv에 남는가남음남지 않음

build-system 설정

[build-system]
requires = ["uv_build>=0.11.3,<0.12"]
build-backend = "uv_build"

build-system 없이는 단축키 설정 불가능

  • [build-system]이 없으면 의존성만 설치해서 .exe도 생기지 않음 (패키징 X)
  • uv init만 하면 이 칸이 빠진 채 만들어지므로, 처음부터 uv init --package로 만들어야 들어간다

tool

[tool.도구이름] 규칙 덕분에 여러 도구가 한 파일(pyproject.toml)을 동시에 사용해도 서로 헷갈리지 않는다. 즉, [tool.XXX] 칸은 그 도구만 읽는 전용 메모 칸이다.

도구별 전용 칸

[tool.uv.build-backend]    # uv만 읽음
[tool.pytest.ini_options]  # pytest만 읽음
[tool.ruff]                # ruff만 읽음

이 규칙에 따라 [tool.uv.build-backend] 칸에는 빌드 담당자 uv_build에게 주는 작업 지시서를 적는다. 아무것도 적지 않으면 uv_build는 다음 기본 규칙으로 동작한다.

키의미기본값
module-root코드가 들어 있는 폴더src
module-namemodule-root 안에서 찾을 패키지 폴더 이름프로젝트 이름

기본 규칙대로라면 uv_build는 src/my-project/__init__.py를 찾는다. 하지만 예시 프로젝트에는 이 경로가 없고, 실제 코드는 src/DataUpload/__init__.py에 있다.

폴더 이름이 프로젝트 이름과 다르므로 module-name에 DataUpload를 직접 지정한다. 반면 module-root는 기본값이 src라서 적지 않았다.

tool.uv.build-backend 설정

[tool.uv.build-backend]
module-name = ["DataUpload"]
# module-root = "src"   ← 기본값이라 생략. 적어도 결과는 같다

작동 방식

앞서 본 설정만으로 명령어가 짧아지는 이유는 설치할 때 src 폴더가 sys.path에 등록되기 때문이다.

Python은 import DataUpload를 만나면 sys.path에 있는 디렉토리들을 차례로 뒤져서 DataUpload를 찾는다. sys.path는 Python이 모듈을 찾아보는 책장인 셈이다.

이전에는 sys.path에 프로젝트 최상위 경로만 있었기 때문에 src.DataUpload라는 경로를 전부 적어야 했다. 빌드 담당자를 설정하고 설치하면 .venv에 아래 .pth 파일이 생기고, 이 파일이 src 폴더를 sys.path에 추가한다.

.pth 파일 내용

.venv/Lib/site-packages/my-project.pth
  내용: C:\...\my-project\src

Python은 시작할 때 이 .pth 쪽지를 읽고 src 안에서부터 모듈을 찾는다. 그래서 DataUpload가 바로 보인다.

Fig 1. .pth 등록 전후의 모듈 탐색 경로
Fig 1. .pth 등록 전후의 모듈 탐색 경로

요약하면,

  1. module-root = "src"(기본값)
  2. .pth에 src 경로 등록
  3. Python이 src 안에서부터 탐색
  4. import DataUpload, "DataUpload.csv_to_snowpg:main"으로 충분

editable 설치

  • .pth 쪽지는 코드를 복사한 게 아니라 원래 폴더를 가리키기만 한다.
  • 그래서 src/DataUpload/ 코드를 고치면 uv sync를 다시 하지 않아도 바로 반영되는데, 이를 editable 설치라고 한다.

활용

단축키를 등록한 뒤에는 인자 기본값으로 입력을 더 줄이고, 필요한 명령어를 한 줄씩 추가할 수 있다.

파라미터 줄이기

명령어가 짧아진 데는 이유가 하나 더 있다. 자주 쓰는 값을 인자의 기본값으로 미리 정해 뒀다. 그래서 인자를 입력하지 않고 실행만 하면 된다.

기본값을 둔 인자 정의

# src/DataUpload/csv_to_snowpg.py
ap.add_argument("files", nargs="*", type=Path, help="기본값: data/*.csv")
ap.add_argument("--dry-run", action="store_true", help="DB 없이 변환/스키마 검증만") # --dry-run 을 넣으면 True 조건

files를 생략하면 data/*.csv를 읽고, --dry-run을 넣으면 DB 없이 변환·스키마 검증만 한다. 인자(argument) 정의는 argparse 가이드를 참고한다.

단축키 추가하기

새 명령어가 필요하면 [project.scripts]에 한 줄만 추가하면 된다. 예를 들어 src/DataUpload/check.py에 main()이 있다면 아래처럼 적는다.

project.scripts에 단축키 추가

[project.scripts]
csv-to-snowpg = "db.csv_to_snowpg:main"
db-check      = "db.check:main"

동기화 후 실행

uv sync
uv run db-check

수정 후 uv sync 재실행 필수

실행 파일(.exe)은 설치할 때만 생긴다. [project.scripts]를 고친 다음 uv sync를 하지 않으면 새 명령어를 찾지 못한다.


마치며

uv sync 한 번에 일어나는 일과 단축 명령어가 실행되는 흐름을 정리하면 다음과 같다.

Fig 2. uv sync의 동작과 단축 명령어 실행 흐름
Fig 2. uv sync의 동작과 단축 명령어 실행 흐름

Reference