[Python] Package, PyPI, pip, pyproject.toml

4 minute read

Published:

In this post, concepts of python package is introduced.

Python package

디렉터리 구조가 아래와 같다고 하자.

my_project/
├── test.py
└── mylib/
    ├── __init__.py
    └── hello.py
# hello.py
def hi():
    print("Hello")
# test.py
from mylib.hello import hi

hi()

터미널에서 다음을 실행하면 정상 동작한다.

cd my_project
python test.py

왜냐하면 python은 import한 package를 현재 실행 중인 디렉터리(my_project) 안에서 먼저 찾아보기 때문이다. 그래서, my_project 에서 my_lib을 발견하여 import가 된다.

이번에는 디렉터리 구조가 아래와 같다고 하자.

workspace/
├── app/
│   └── test.py
└── libraries/
    └── mylib/
        ├── __init__.py
        └── hello.py
# test.py
from mylib.hello import hi

hi()

터미널에서 다음을 실행하면 ModuleNotFoundError 가 난다. python은 기본적으로 현재 실행 중인 폴더(workspace/app) 만 찾아보고 자동으로 ../libraries 까지는 가지 않기 때문이다.

cd workspace/app
python test.py

해결방법은 몇 가지가 있다.

  • 설치 : pip install -e ../libraries/mylib 을 한다. 그러면 python은 mylib가 workspace/libraries/mylib 에 있다는 것을 기억한다. 이제 어디서든 import mylib 이 가능하다.
  • PYTHONPATH 추가 : export PYTHONPATH=../libraries 처럼 경로를 직접 추가한다.

참고로, __init__.py는 python이 찾은 폴더가 패키지가 맞음을 확인시켜주는 역할을 한다.

PyPI, pip, uv, package build

pip 는 python 프로젝트를 설치하는 프로그램이다. 그리고 PyPI 는 python 오픈소스 프로젝트 저장소이다. pip install numpy를 하면 pip는 PyPI에서 numpy를 다운로드하여 python이 import 할 수 있도록 설치한다. 위에서 본 것처럼 내가 작성한 패키지도 import를 위해서 pip install 할 수 있다.

참고로, pip install -e에서 -e는 editable 옵션이다. -e 옵션을 하지 않고, pip install ../mylib라 하면 python은 mylib를 복사해서 python 설치폴더(site-packages)에 넣는다. 그러면 원본 파일을 수정하면, install을 다시해야 한다. -e옵션을 사용하면 복사대신 경로를 기억하므로 수정 후, 다시 install 할 필요가 없다.

uv 는 pip보다 빠른 최신 python 패키지 관리자로, 설치 뿐만 아니라, 가상환경 생성, 의존성 관리, lock 파일 생성 등을 모두 해준다.

python에서 build란, 크게 두 가지 의미가 있다.

  • 배포용 빌드(build)어란, python -m build와 같이 실행해, robotescape-0.1.0.whl, robotescape-0.1.0.tar.gz와 같은 배포 파일을 만드는 것이다. C++처럼 실행 파일을 만드는 것이 아니라, 프로젝트를 pip install 할 수 있는 형태(일반적으로 바로 pip install robotescape을 할 수 없다.) 만드는 것을 의미한다.
  • 설치용(install) 빌드란, uv sync, pip install . 와 같이 실행해, 다운 받은 프로젝트 패키지가 실행 가능하게 만든다. 이때 사용되는 도구가 setuptools와 같은 빌드 도구인데, 이들은 pyproject.toml에 적힌 내용을 보고 import mylib을 해야함을 알고, mylib에 대해 mylib-0.1.0-py3-none-any.whl 와 같은 whl 파일을 만든다. 그제서야 pip install mylib-0.1.0-py3-none-any.whl이 가능하다. (이 과정을 whl파일 흔적을 남기지 않고 uv sync가 한다.) (pip install -e mylib을 하면 빌드를 내가 직접하지 않아도 알아서 해준다.) pip install numpy를 할때에도 실제로는 .whl파일을 다운받아 오는 것이다. pip install mylib-0.1.0.whl을 하면 wheel 압축파일을 풀어, site-packages로 풀린 결과가 여러가지 메타데이터와 함께 들어가게 된다.

pyproject.toml

pyproject.toml은 이 프로젝트는 어떤 프로젝트인지, 어떻게 설치하고, 무엇이 필요한지를 적어놓은 파일이다. 이 파일은 여러 프로그램이 읽는다. 다음 예시를 보자.

[project]
name = "robotescape"
version = "0.1.0"
description = "Robot Room Escape Benchmark meta package."
readme = "README.md"
requires-python = "==3.12.*"
dependencies = [
    "robotescape-core==0.1.0",
]

[project.optional-dependencies]
isaac = [
    "robotescape-isaac==0.1.0",
]
ros2 = [
    "robotescape-ros2==0.1.0",
]
docs = [
    "furo>=2025.12.19",
    "sphinx>=9.0.4",
]
test = [
    "pytest>=8.0.0",
    "robotescape-ros2==0.1.0",
]

[build-system]
requires = ["setuptools>=69"]
build-backend = "setuptools.build_meta"

[tool.setuptools]
packages = []

[tool.uv.sources]
robotescape-core = { path = "packages/robotescape-core", editable = true }
robotescape-isaac = { path = "packages/robotescape-isaac", editable = true }
robotescape-ros2 = { path = "packages/robotescape-ros2", editable = true }
isaacsim = { index = "nvidia" }
isaaclab = { index = "nvidia" }
torch = { index = "pytorch-cu130" }
torchvision = { index = "pytorch-cu130" }
torchaudio = { index = "pytorch-cu130" }

[tool.uv]
index-strategy = "unsafe-best-match"
prerelease = "allow"

[[tool.uv.index]]
name = "pypi"
url = "https://pypi.org/simple"
default = true

[[tool.uv.index]]
name = "pytorch-cu130"
url = "https://download.pytorch.org/whl/cu130"
explicit = true

[[tool.uv.index]]
name = "nvidia"
url = "https://pypi.nvidia.com"
explicit = false

[tool.pytest.ini_options]
pythonpath = [
    "packages/robotescape-core/src",
    "packages/robotescape-ros2/src",
    "ros2/robotescape_teleop",
]
testpaths = [
    "tests",
    "packages/robotescape-isaac/tests",
    "ros2/robotescape_teleop/test",
]

예를 들어, uv sync를 하면, 필요한 다른 프로젝트를 설치하고 프로젝트가 빌드된다. 이때, pyproject.toml을 읽고 다음의 일들을 한다.

  • [project] 섹션은 프로젝트 자체에 대한 표준 정보를 담고 있다. uv sync는 특히 dependencies를 보고 robotescape를 설치용 빌드하려면 robotescape-core 0.1.0도 반드시 필요함을 알게 된다. (project 섹션의 내용은 배포용 빌드 때에도 쓰인다.)
  • [project.optional-dependencies] 섹션은 선택 설치가 가능한 패키지를 명시한다. 예를 들어, uv sync --extra isaac으로 실행하면, robot escape-isaac 패키지도 설치한다.
  • [build-system] 섹션은 프로젝트를 Python 패키지 형태로 빌드할 때 setuptools를 쓰라는 것이다.
  • [tools.setuptools] 섹션은 setuptools 전용 설정이다. packages = [] 의 의미는 최상위 robotescape 프로젝트 자체에는 실제 import할 python 코드 패키지가 없음을 의미한다.
  • [tool.uv.sources] 섹션을 통해서 uv는 프로젝트를 설치함에 있어서 필요한 robotescape-core는 인터넷에서 받지 말고, 현재 저장소의 packages/robotescape-core를 사용해야 함을 알 수 있다.
  • [tool.pytest.ini_options] 섹션은 uv가 아니라 pytest를 실행할 때 적용된다. pytest를 실행하면 테스트를 실행하는 동안 pythonpath의 경로들을 python import 검색 경로에 넣는다. 그래서 test코드에서 import robotescape_core 같은 것을 할 수 있게 도와준다. testpaths는 pytest에게 테스트 파일을 우선 이 디렉터리들에서 찾으라고 알려준다.

Leave a Comment