12. 패키징 및 배포 (Packaging & Distribution)

12. 패키징 및 배포 (Packaging & Distribution)

학습 목표

  • Python 패키지 구조와 표준 이해
  • pyproject.toml을 활용한 현대적 패키지 설정
  • Poetry를 활용한 의존성 관리
  • PyPI 배포 과정 이해
  • 버전 관리 및 릴리스 자동화

목차

  1. 패키지 구조
  2. pyproject.toml
  3. 의존성 관리
  4. Poetry 활용
  5. PyPI 배포
  6. 버전 관리
  7. 연습 문제

1. 패키지 구조

1.1 표준 프로젝트 구조

mypackage/
├── src/
   └── mypackage/
       ├── __init__.py
       ├── core.py
       ├── utils.py
       └── cli.py
├── tests/
   ├── __init__.py
   ├── conftest.py
   ├── test_core.py
   └── test_utils.py
├── docs/
   ├── index.md
   └── api.md
├── pyproject.toml          # 핵심 설정 파일
├── README.md
├── LICENSE
├── CHANGELOG.md
└── .gitignore

1.2 src 레이아웃 vs Flat 레이아웃

# src 레이아웃 (권장)
mypackage/
├── src/
   └── mypackage/        # 패키지가 src/ 아래
       └── __init__.py
└── tests/

# Flat 레이아웃
mypackage/
├── mypackage/            # 패키지가 루트에
   └── __init__.py
└── tests/
src 레이아웃의 장점:
┌─────────────────────────────────────────────────────────────────┐
 1. 설치된 패키지와 개발 중인 패키지 구분 명확                      
 2. 설치 없이 import  에러 (의도된 동작)                        
 3. 테스트가 설치된 패키지를 테스트하도록 보장                      
 4. 패키지 이름과 프로젝트 루트 이름 충돌 방지                     
└─────────────────────────────────────────────────────────────────┘

1.3 init.py 작성

# src/mypackage/__init__.py
"""
MyPackage - 패키지 설명

사용 예시:
    >>> from mypackage import Calculator
    >>> calc = Calculator()
    >>> calc.add(2, 3)
    5
"""

from mypackage.core import Calculator
from mypackage.utils import format_number

__version__ = "1.0.0"
__author__ = "Your Name"
__all__ = ["Calculator", "format_number"]

2. pyproject.toml

2.1 기본 구조 (PEP 621)

# pyproject.toml
[build-system]
requires = ["setuptools>=61.0", "wheel"]
build-backend = "setuptools.build_meta"

[project]
name = "mypackage"
version = "1.0.0"
description = "A sample Python package"
readme = "README.md"
license = {text = "MIT"}
requires-python = ">=3.9"
authors = [
    {name = "Your Name", email = "you@example.com"}
]
maintainers = [
    {name = "Maintainer", email = "maintainer@example.com"}
]
keywords = ["sample", "package", "python"]
classifiers = [
    "Development Status :: 4 - Beta",
    "Intended Audience :: Developers",
    "License :: OSI Approved :: MIT License",
    "Operating System :: OS Independent",
    "Programming Language :: Python :: 3",
    "Programming Language :: Python :: 3.9",
    "Programming Language :: Python :: 3.10",
    "Programming Language :: Python :: 3.11",
    "Programming Language :: Python :: 3.12",
]

dependencies = [
    "requests>=2.28.0",
    "pydantic>=2.0",
]

[project.optional-dependencies]
dev = [
    "pytest>=7.0",
    "pytest-cov",
    "black",
    "mypy",
    "ruff",
]
docs = [
    "mkdocs",
    "mkdocs-material",
]

[project.urls]
Homepage = "https://github.com/username/mypackage"
Documentation = "https://mypackage.readthedocs.io"
Repository = "https://github.com/username/mypackage.git"
Changelog = "https://github.com/username/mypackage/blob/main/CHANGELOG.md"

[project.scripts]
mypackage-cli = "mypackage.cli:main"

[project.entry-points."mypackage.plugins"]
plugin1 = "mypackage.plugins.plugin1:Plugin1"

2.2 동적 버전 관리

# pyproject.toml
[project]
name = "mypackage"
dynamic = ["version"]

[tool.setuptools.dynamic]
version = {attr = "mypackage.__version__"}
# src/mypackage/__init__.py
__version__ = "1.0.0"

2.3 도구별 설정

# pyproject.toml - 도구 설정 통합

# Black (코드 포매터)
[tool.black]
line-length = 88
target-version = ["py39", "py310", "py311"]
include = '\.pyi?$'
exclude = '''
/(
    \.git
    | \.venv
    | build
    | dist
)/
'''

# Ruff (린터)
[tool.ruff]
line-length = 88
select = ["E", "F", "W", "I", "N", "B", "A"]
ignore = ["E501"]
target-version = "py39"

[tool.ruff.isort]
known-first-party = ["mypackage"]

# MyPy (타입 검사)
[tool.mypy]
python_version = "3.9"
warn_return_any = true
warn_unused_ignores = true
disallow_untyped_defs = true
plugins = ["pydantic.mypy"]

[[tool.mypy.overrides]]
module = "tests.*"
disallow_untyped_defs = false

# pytest
[tool.pytest.ini_options]
testpaths = ["tests"]
addopts = "-v --cov=mypackage --cov-report=term-missing"

# Coverage
[tool.coverage.run]
source = ["src/mypackage"]
omit = ["*/tests/*"]

[tool.coverage.report]
fail_under = 80

3. 의존성 관리

3.1 의존성 유형

[project]
# 런타임 필수 의존성
dependencies = [
    "requests>=2.28.0,<3.0",
    "pydantic>=2.0",
    "python-dateutil",
]

[project.optional-dependencies]
# 개발용
dev = [
    "pytest>=7.0",
    "pytest-cov",
    "black",
    "ruff",
    "mypy",
    "pre-commit",
]
# 문서화
docs = [
    "mkdocs>=1.4",
    "mkdocs-material",
]
# 특정 기능용
postgresql = ["psycopg2-binary>=2.9"]
mysql = ["mysqlclient>=2.1"]
all = [
    "mypackage[postgresql,mysql]",
]

3.2 버전 지정 문법

# 버전 지정 예시
package>=1.0          # 1.0 이상
package>=1.0,<2.0     # 1.0 이상 2.0 미만
package~=1.4.2        # >=1.4.2, ==1.4.* (호환 릴리스)
package==1.4.2        # 정확히 1.4.2
package!=1.5.0        # 1.5.0 제외

# 환경 마커
package; python_version >= "3.10"
package; sys_platform == "win32"

3.3 requirements.txt와 연동

# pyproject.toml에서 requirements.txt 생성
pip-compile pyproject.toml -o requirements.txt

# 개발 의존성 포함
pip-compile pyproject.toml --extra dev -o requirements-dev.txt

4. Poetry 활용

4.1 Poetry 설치 및 초기화

# 설치 (공식 방법)
curl -sSL https://install.python-poetry.org | python3 -

# 새 프로젝트 생성
poetry new mypackage

# 기존 프로젝트에 Poetry 추가
cd existing-project
poetry init

4.2 Poetry pyproject.toml

# pyproject.toml (Poetry 형식)
[tool.poetry]
name = "mypackage"
version = "1.0.0"
description = "A sample package"
authors = ["Your Name <you@example.com>"]
license = "MIT"
readme = "README.md"
homepage = "https://github.com/username/mypackage"
repository = "https://github.com/username/mypackage"
documentation = "https://mypackage.readthedocs.io"
keywords = ["sample", "package"]
classifiers = [
    "Development Status :: 4 - Beta",
    "Intended Audience :: Developers",
]
packages = [{include = "mypackage", from = "src"}]

[tool.poetry.dependencies]
python = "^3.9"
requests = "^2.28"
pydantic = "^2.0"

[tool.poetry.group.dev.dependencies]
pytest = "^7.0"
pytest-cov = "^4.0"
black = "^23.0"
mypy = "^1.0"
ruff = "^0.1"

[tool.poetry.group.docs.dependencies]
mkdocs = "^1.4"
mkdocs-material = "^9.0"

[tool.poetry.scripts]
mypackage-cli = "mypackage.cli:main"

[build-system]
requires = ["poetry-core>=1.0.0"]
build-backend = "poetry.core.masonry.api"

4.3 Poetry 명령어

# 의존성 설치
poetry install                  # 모든 의존성
poetry install --only main      # 메인 의존성만
poetry install --with dev,docs  # 특정 그룹 포함

# 의존성 추가/제거
poetry add requests             # 메인 의존성
poetry add pytest --group dev   # 개발 의존성
poetry remove requests

# 의존성 업데이트
poetry update                   # 모든 패키지
poetry update requests          # 특정 패키지

# 가상환경
poetry env info                 # 환경 정보
poetry env use python3.11       # Python 버전 지정
poetry shell                    # 가상환경 활성화

# 빌드 및 배포
poetry build                    # 빌드 (wheel + sdist)
poetry publish                  # PyPI 배포
poetry publish --dry-run        # 테스트 실행

# 버전 관리
poetry version patch            # 0.1.0 -> 0.1.1
poetry version minor            # 0.1.0 -> 0.2.0
poetry version major            # 0.1.0 -> 1.0.0

# 스크립트 실행
poetry run pytest               # 테스트 실행
poetry run mypackage-cli        # CLI 실행

4.4 poetry.lock

# lock 파일 생성/업데이트
poetry lock

# lock 파일 없이 설치 (비권장)
poetry install --no-lock

# lock 파일만 업데이트 (설치 안 함)
poetry lock --no-update

5. PyPI 배포

5.1 배포 준비

# 빌드 도구 설치
pip install build twine

# 빌드
python -m build

# 결과물 확인
ls dist/
# mypackage-1.0.0-py3-none-any.whl
# mypackage-1.0.0.tar.gz

5.2 TestPyPI 테스트

# TestPyPI에 업로드
twine upload --repository testpypi dist/*

# TestPyPI에서 설치 테스트
pip install --index-url https://test.pypi.org/simple/ mypackage

5.3 PyPI 배포

# PyPI에 업로드
twine upload dist/*

# 또는 Poetry 사용
poetry publish

# API 토큰 사용 (권장)
# ~/.pypirc
[pypi]
username = __token__
password = pypi-AgEIcH...

# 또는 환경 변수
export TWINE_USERNAME=__token__
export TWINE_PASSWORD=pypi-AgEIcH...

5.4 GitHub Actions 자동 배포

# .github/workflows/publish.yml
name: Publish to PyPI

on:
  release:
    types: [published]

jobs:
  publish:
    runs-on: ubuntu-latest
    environment: release
    permissions:
      id-token: write  # Trusted Publisher

    steps:
      - uses: actions/checkout@v4

      - name: Set up Python
        uses: actions/setup-python@v5
        with:
          python-version: "3.11"

      - name: Install dependencies
        run: |
          pip install build

      - name: Build package
        run: python -m build

      - name: Publish to PyPI
        uses: pypa/gh-action-pypi-publish@release/v1
        # Trusted Publisher 사용 시 토큰 불필요

6. 버전 관리

6.1 Semantic Versioning (SemVer)

버전 형식: MAJOR.MINOR.PATCH

MAJOR: 호환되지 않는 API 변경
MINOR: 하위 호환 기능 추가
PATCH: 하위 호환 버그 수정

예시:
1.0.0 → 1.0.1 (버그 수정)
1.0.1 → 1.1.0 (새 기능 추가)
1.1.0 → 2.0.0 (Breaking change)

Pre-release:
1.0.0-alpha.1
1.0.0-beta.1
1.0.0-rc.1

6.2 CHANGELOG 작성

# Changelog

All notable changes to this project will be documented in this file.

The format is based on [Keep a Changelog](https://keepachangelog.com/),
and this project adheres to [Semantic Versioning](https://semver.org/).

## [Unreleased]

### Added
- New feature X

### Changed
- Updated dependency Y

## [1.1.0] - 2024-03-15

### Added
- Added support for Python 3.12
- New `validate()` method in Calculator class

### Fixed
- Fixed division by zero handling

### Deprecated
- `old_method()` will be removed in 2.0.0

## [1.0.0] - 2024-01-01

### Added
- Initial release
- Calculator class with basic operations
- CLI interface

6.3 자동 버전 관리 (bump2version)

# 설치
pip install bump2version

# 설정 파일
# .bumpversion.cfg
[bumpversion]
current_version = 1.0.0
commit = True
tag = True

[bumpversion:file:src/mypackage/__init__.py]
[bumpversion:file:pyproject.toml]

# 사용
bump2version patch  # 1.0.0 -> 1.0.1
bump2version minor  # 1.0.1 -> 1.1.0
bump2version major  # 1.1.0 -> 2.0.0

7. 연습 문제

연습 1: pyproject.toml 작성

간단한 유틸리티 패키지의 pyproject.toml을 작성하세요.

# 예시 답안
[build-system]
requires = ["setuptools>=61.0"]
build-backend = "setuptools.build_meta"

[project]
name = "myutils"
version = "0.1.0"
description = "My utility functions"
readme = "README.md"
requires-python = ">=3.9"
authors = [{name = "Your Name", email = "you@example.com"}]
license = {text = "MIT"}
dependencies = []

[project.optional-dependencies]
dev = ["pytest>=7.0", "black", "mypy"]

[tool.setuptools.packages.find]
where = ["src"]

연습 2: Poetry 프로젝트 설정

Poetry로 새 프로젝트를 생성하고 의존성을 관리하세요.

# 예시 답안
poetry new myproject --src
cd myproject

# 의존성 추가
poetry add requests pydantic
poetry add --group dev pytest black mypy

# 빌드
poetry build

연습 3: CLI 엔트리포인트

CLI 도구를 만들고 pyproject.toml에 등록하세요.

# src/mypackage/cli.py
import argparse


def main():
    parser = argparse.ArgumentParser(description="My CLI tool")
    parser.add_argument("name", help="Your name")
    parser.add_argument("-g", "--greeting", default="Hello")
    args = parser.parse_args()

    print(f"{args.greeting}, {args.name}!")


if __name__ == "__main__":
    main()
# pyproject.toml
[project.scripts]
greet = "mypackage.cli:main"
# 설치 후 실행
pip install -e .
greet World
# Hello, World!

다음 단계

참고 자료

to navigate between lessons