Following system colour scheme Selected dark colour scheme Selected light colour scheme

Python 개선 제안 한국어 번역

PEP 735 – pyproject.toml의 의존성 그룹

Author:
Stephen Rosen <sirosen0 at gmail.com>
Sponsor:
Brett Cannon <brett at python.org>
PEP-Delegate:
Paul Moore <p.f.moore at gmail.com>
Discussions-To:
Discourse thread
Status:
Final
Type:
Standards Track
Topic:
Packaging
Created:
20-Nov-2023
Post-History:
14-Nov-2023, 20-Nov-2023
Resolution:
10-Oct-2024

Table of Contents

번역·라이선스 안내

이 비공식 한국어 번역은 원문 Copyright 절의 Public Domain or CC0-1.0, whichever is more permissive 조건에 따라 제공합니다. 원저자와 공식 원문은 그대로 표시합니다. 수정되지 않은 기준 원문 · 공식 최신판

Important

This PEP is a historical document. The up-to-date, canonical spec, Dependency Groups, is maintained on the PyPA specs page.

×

See the PyPA specification update process for how to propose changes.

초록

이 PEP는 pyproject.toml 파일에 패키지 요구 사항을 저장하되 프로젝트의 어떤 빌드된 배포물에도 포함되지 않도록 하는 메커니즘을 규정합니다.

이는 requirements.txt 파일과 유사하게, 런처, IDE 및 기타 도구가 이름으로 찾고 식별할 수 있는 이름 붙은 의존성 그룹을 만드는 데 적합합니다.

여기에서 정의하는 기능을 “의존성 그룹(Dependency Groups)”이라고 부릅니다.

동기

Python 커뮤니티에 표준화된 해답이 없는 주요 사용 사례가 두 가지 있습니다:

  • 패키지의 개발 의존성은 어떻게 정의해야 합니까?
  • 배포물을 빌드하지 않는 프로젝트(비패키지 프로젝트)의 의존성은 어떻게 정의해야 합니까?

이 두 가지 요구를 뒷받침하는 일반적인 해법으로, 이 제안과 유사한 것이 두 가지 있습니다:

  • requirements.txt 파일
  • 패키지 extras

requirements.txt 파일과 extras에는 모두 이 표준이 극복하고자 하는 한계가 있습니다.

위의 두 사용 사례는 이 PEP가 지원하고자 하는 서로 다른 두 가지 유형의 프로젝트를 설명한다는 점에 유의하십시오:

  • 라이브러리와 같은 Python 패키지
  • 데이터 과학 프로젝트와 같은 비패키지 프로젝트

이 제안의 동기가 되는 여러 사용 사례는 사용 사례 부록에 자세히 정의되어 있습니다.

requirements.txt 파일의 한계

많은 프로젝트는 하나 이상의 requirements.txt 파일을 정의할 수 있으며, 이를 프로젝트 루트(예: requirements.txt 및 test-requirements.txt)에 두거나 디렉터리(예: requirements/base.txt 및 requirements/test.txt) 안에 배치할 수 있습니다. 그러나 이런 방식으로 요구 사항 파일을 사용하는 데에는 중대한 문제가 있습니다:

  • 도구가 이러한 파일을 이름으로 발견하거나 사용할 수 있게 하는 표준화된 명명 규칙이 없습니다.
  • requirements.txt 파일은 표준화되어 있지 않으며, 대신 pip에 옵션을 제공합니다.

그 결과, requirements.txt 파일을 기반으로 도구의 동작을 정의하기가 어렵습니다. 이 파일들은 이름으로 발견하거나 식별하기가 간단하지 않으며, 그 내용에는 패키지 지정자와 추가적인 pip 옵션이 섞여 있을 수 있습니다.

또한 requirements.txt의 내용에 대한 표준이 없다는 것은, 이 파일들을 pip 이외에 이를 처리하려는 다른 대체 도구로 이식할 수 없다는 뜻이기도 합니다.

또한 requirements.txt 파일은 의존성 목록마다 파일 하나를 필요로 합니다. 일부 사용 사례에서는 이로 인해 의존성 그룹화의 한계 비용이 그 이점에 비해 높아집니다. 소규모 의존성 그룹을 여러 개 가진 프로젝트에는 더 간결한 선언이 유리합니다.

이와 대조적으로, 의존성 그룹은 pyproject.toml 내의 잘 알려진 위치에 완전히 표준화된 내용으로 정의됩니다. 의존성 그룹은 즉각적인 유용성을 가질 뿐만 아니라, 향후 표준을 위한 출발점 역할도 할 것입니다.

extras의 한계

extras는 [project.optional-dependencies] 테이블에 선언되는 추가 패키지 메타데이터입니다. 이들은 패키지 메타데이터의 일부로 게시되는 패키지 지정자 목록에 이름을 부여하며, 사용자는 그 이름으로 해당 목록을 요청할 수 있습니다. 예를 들어 pip install 'foo[bar]'는 foo를 bar extra와 함께 설치합니다.

extras는 패키지 메타데이터이므로 정적으로 정의된다는 보장이 없으며, 이를 해석하려면 빌드 시스템이 필요할 수 있습니다. 또한 [project.optional-dependencies]를 정의하면 많은 도구가 해당 프로젝트를 패키지로 간주하며, 이는 [project] 테이블의 유효성 검사와 같은 도구 동작으로 이어질 수 있습니다.

패키지인 프로젝트에서는 extras가 개발 의존성을 정의하는 일반적인 방법이지만, 이러한 경우에도 단점이 있습니다:

  • extra는 선택적인 추가 의존성을 정의하므로, 현재 패키지와 그 의존성을 설치하지 않고 extra만 설치할 수는 없습니다.
  • extras는 사용자가 설치할 수 있으므로 패키지의 공개 인터페이스에 속합니다. extras는 게시되므로, 패키지 개발자는 개발용 extras가 사용자용 extras와 혼동되지 않도록 신경 쓰는 경우가 많습니다.

근거

이 PEP는 요구 사항 데이터를 [dependency-groups] 테이블 안의 목록으로 저장하는 방식을 정의합니다. 이 이름은 이 기능의 정식 명칭(“의존성 그룹”)과 일치하도록 선택되었습니다.

이 형식은 가능한 한 단순하고 배우기 쉬워야 하며, 많은 경우 기존 requirements.txt 파일과 매우 비슷한 형식을 갖습니다. [dependency-groups]의 각 목록은 패키지 지정자의 목록으로 정의됩니다. 예를 들면 다음과 같습니다:

[dependency-groups]
test = ["pytest>7", "coverage"]

requirements.txt 파일의 사용 사례 중에는 PEP 508 의존성 지정자로 표현할 수 없는 데이터가 필요한 경우가 여럿 있습니다. 이러한 필드는 의존성 그룹에서 유효하지 않습니다. 색인 서버, 해시, 경로 의존성처럼 pip가 지원하는 여러 데이터와 필드를 포함하려면 새로운 표준이 필요합니다. 이 표준은 새로운 표준과 발전의 여지를 남겨 두지만, 유효한 requirements.txt 내용을 모두 지원하려고 하지는 않습니다.

이에 대한 유일한 예외는 requirements.txt 파일이 한 파일을 다른 파일에 포함할 때 사용하는 -r 플래그입니다. Dependency Groups는 이와 의미가 비슷한 “include” 메커니즘을 지원하며, 이를 통해 한 의존성 그룹이 다른 의존성 그룹을 확장할 수 있습니다.

의존성 그룹에는 requirements.txt 파일과 유사한 두 가지 추가 기능이 있습니다.

  • 어떤 빌드된 배포판에서도 별도의 메타데이터로 게시되지 않습니다
  • 의존성 그룹을 설치한다고 해서 패키지의 의존성이나 패키지 자체가 설치되는 것은 아닙니다

사용 사례

다음 사용 사례는 이 PEP의 중요한 목표로 간주됩니다. 이들은 사용 사례 부록에서 더 자세히 정의됩니다.

  • Python 패키징이 아닌 빌드 프로세스를 통해 배포되는 웹 애플리케이션
  • 게시되지 않는 개발용 의존성 그룹이 있는 라이브러리
  • 의존성 그룹은 있지만 핵심 패키지는 없는 데이터 과학 프로젝트
  • 잠금 파일 생성을 위한 입력 데이터 (일반적으로 의존성 그룹을 잠긴 의존성 데이터를 두는 위치로 사용해서는 안 됩니다)
  • tox, Nox, Hatch와 같은 환경 관리자를 위한 입력 데이터
  • 테스트 및 린터 요구 사항에 대한 설정 가능한 IDE 탐색

Poetry와 PDM의 의존성 그룹에 관하여

기존 도구인 Poetry와 PDM은 각각 “의존성 그룹(Dependency Groups)”이라고 부르는 기능을 이미 제공합니다. 그러나 의존성 모음을 지정하는 표준이 없기 때문에, 각 도구는 [tool] 테이블의 관련 섹션에서 도구별 방식으로 이를 정의합니다.

(PDM은 일부 의존성 그룹에 엑스트라(extras)도 사용하며, 이 개념은 엑스트라와 상당 부분 겹칩니다.)

이 PEP는 Poetry와 PDM의 모든 기능을 지원하지는 않습니다. 이들 도구는 pip용 requirements.txt 파일과 마찬가지로 일반적인 의존성 지정자에 대한 여러 비표준 확장을 지원합니다.

이러한 도구는 표준화된 의존성 그룹을 자체 의존성 그룹 메커니즘의 확장으로 사용할 수 있어야 합니다. 그러나 기존 Poetry와 PDM의 방식을 대체하는 새로운 데이터 형식을 정의하는 것은 이 PEP의 목표가 아닙니다. 그렇게 하려면 경로 의존성처럼 이들 도구가 지원하는 여러 추가 기능을 표준화해야 합니다.

의존성 그룹은 숨겨진 엑스트라가 아닙니다

의존성 그룹은 게시되지 않는 엑스트라와 매우 비슷합니다. 그러나 의존성 그룹을 엑스트라와 더 확실히 구별하는 두 가지 주요 특징이 있습니다.

  • 의존성 그룹은 패키지가 아닌 프로젝트를 지원합니다
  • 의존성 그룹을 설치해도 패키지의 의존성(또는 패키지 자체)이 함께 설치되지는 않습니다

향후 호환성 및 유효하지 않은 데이터

의존성 그룹은 향후 PEP에서 확장할 수 있도록 설계되었습니다. 그러나 의존성 그룹은 하나의 Python 프로젝트 안에서 여러 도구가 함께 사용할 수도 있어야 합니다. 여러 도구가 같은 데이터를 사용하면, 한 도구는 의존성 그룹을 확장하는 향후 PEP를 구현하고 다른 도구는 구현하지 않을 수 있습니다.

이런 경우에 사용자를 지원하기 위해, 이 PEP는 도구가 자신이 사용하는 의존성 그룹만 검사하는 유효성 검사 동작을 정의하고 권장합니다. 이를 통해 서로 다른 버전의 의존성 그룹 데이터를 사용하는 여러 도구가 pyproject.toml의 단일 테이블을 공유할 수 있습니다.

명세

이 PEP는 pyproject.toml 파일에 dependency-groups라는 이름의 새 섹션(테이블)을 정의합니다. dependency-groups 테이블에는 사용자가 정의한 키가 개수 제한 없이 들어갈 수 있으며, 각 키의 값은 요구 사항 목록(아래에서 정의)입니다. 이 키는 유효한 비정규화 이름이어야 하며, 비교하기 전에 정규화되어야 합니다.

도구는 기본적으로 정규화되지 않은 원래 이름을 사용자에게 표시하는 것을 우선해야 합니다(SHOULD). 정규화한 후 중복된 이름이 발견되면, 도구는 오류를 내야 합니다(SHOULD).

dependency-groups 아래의 요구 사항 목록에는 문자열, 테이블(Python의 “dict”), 또는 문자열과 테이블이 섞여 들어갈 수 있습니다.

요구 사항 목록의 문자열은 PEP 508에 정의된 유효한 의존성 지정자여야 합니다.

요구 사항 목록의 테이블은 유효한 의존성 객체 지정자여야 합니다.

의존성 객체 지정자

의존성 객체 지정자는 0개 이상의 의존성을 정의하는 테이블입니다.

이 PEP는 의존성 객체 지정자 중 “의존성 그룹 포함(Dependency Group Include)” 한 가지 유형만 표준화합니다. 다른 유형은 향후 표준에서 추가될 수 있습니다.

의존성 그룹 포함

의존성 그룹 포함은 다른 의존성 그룹의 의존성을 현재 의존성 그룹에 포함합니다.

포함은 정확히 하나의 키 "include-group"만을 가지는 테이블로 정의되며, 그 값은 다른 의존성 그룹의 이름을 나타내는 문자열입니다.

예를 들어 {include-group = "test"}는 test 의존성 그룹의 내용으로 확장되는 포함입니다.

포함은 지정된 의존성 그룹의 내용을 현재 그룹의 포함 위치에 삽입한 것과 정확히 동일하도록 정의됩니다. 예를 들어 foo = ["a", "b"]가 한 그룹이고 bar = ["c", {include-group = "foo"}, "d"]가 또 다른 그룹이라면, 의존성 그룹 포함을 확장했을 때 bar는 ["c", "a", "b", "d"]로 평가되어야 합니다.

의존성 그룹 포함은 같은 패키지를 여러 번 지정할 수 있습니다. 도구는 포함으로 생성된 리스트 내용의 중복을 제거하거나 달리 변경해서는 안 됩니다(SHOULD NOT). 예를 들어 다음과 같은 테이블이 주어졌다고 합시다:

[dependency-groups]
group-a = ["foo"]
group-b = ["foo>1.0"]
group-c = ["foo<1.0"]
all = ["foo", {include-group = "group-a"}, {include-group = "group-b"}, {include-group = "group-c"}]

all의 해석된 값은 ["foo", "foo", "foo>1.0", "foo<1.0"]이어야 합니다(SHOULD). 도구는 이러한 리스트를, 같은 요구 사항을 서로 다른 버전 제약 조건으로 여러 번 처리하도록 요청받는 다른 모든 경우와 정확히 같은 방식으로 처리해야 합니다.

의존성 그룹 포함은 의존성 그룹 포함이 들어 있는 리스트를 포함할 수 있으며, 이 경우 해당 포함들도 함께 확장되어야 합니다. 의존성 그룹 포함은 순환을 포함해서는 안 되며(MUST NOT), 도구는 순환을 감지하면 오류를 보고해야 합니다(SHOULD).

의존성 그룹 테이블 예시

다음은 이를 사용해 네 개의 의존성 그룹 test, docs, typing, typing-test를 정의하는 pyproject.toml의 일부 예시입니다:

[dependency-groups]
test = ["pytest", "coverage"]
docs = ["sphinx", "sphinx-rtd-theme"]
typing = ["mypy", "types-requests"]
typing-test = [{include-group = "typing"}, {include-group = "test"}, "useful-types"]

이 의존성 그룹 선언들 중 어느 것도 현재 패키지, 그 의존성, 또는 선택적 의존성을 암묵적으로 설치하지 않는다는 점에 유의하십시오. test와 같은 의존성 그룹을 사용해 패키지를 테스트하려면 사용자의 설정이나 툴체인이 현재 패키지(.)도 함께 설치해야 합니다. 예를 들어,

$TOOL install-dependency-group test
pip install -e .

위와 같이 하여 ($TOOL이 의존성 그룹 설치를 지원하는 도구라고 가정하면) 테스트 환경을 구축할 수 있습니다.

이를 통해 프로젝트를 패키지로 설치하지 않고도 docs 의존성 그룹을 사용할 수 있습니다:

$TOOL install-dependency-group docs

패키지 빌드

빌드 백엔드는 빌드된 배포물에 의존성 그룹 데이터를 패키지 메타데이터로 포함해서는 안 됩니다(MUST NOT). 이는 sdist의 PKG-INFO와 wheel의 METADATA에 의존성 그룹을 담은 참조 가능한 메타데이터 필드가 전혀 포함되지 않는다는 뜻입니다.

동적 메타데이터를 평가할 때 의존성 그룹을 사용하는 것은 유효하며, sdist에 포함된 pyproject.toml 파일에는 당연히 [dependency-groups] 테이블이 그대로 들어 있습니다. 그러나 이 테이블의 내용은 게시된 패키지의 인터페이스에 속하지 않습니다.

의존성 그룹 설치

의존성 그룹을 지원하는 도구는 사용자가 의존성 그룹으로부터 설치할 수 있도록 새로운 옵션과 인터페이스를 제공할 것으로 기대됩니다.

다음 두 가지 이유로, 패키지의 의존성 그룹을 표현하는 문법은 정의하지 않습니다:

  • PyPI에 있는 서드파티 패키지의 의존성 그룹을 참조하는 것은 유효하지 않습니다(해당 데이터는 게시되지 않는 것으로 정의되어 있기 때문입니다).
  • 의존성 그룹에 대해 현재 패키지가 존재한다는 보장이 없습니다 – 의존성 그룹의 목적 중 하나는 패키지가 아닌 프로젝트를 지원하는 것입니다.

예를 들어, 의존성 그룹을 설치하기 위한 pip 인터페이스는 다음과 같은 형태가 될 수 있습니다:

pip install --dependency-groups=test,typing

이것은 예시일 뿐이라는 점에 유의하십시오. 이 PEP는 도구가 의존성 그룹 설치를 어떻게 지원해야 하는지에 대해 어떠한 요구 사항도 정하지 않습니다.

엑스트라와 겹치는 설치 UX

도구는 엑스트라를 설치할 때와 동일한 인터페이스를 의존성 그룹 설치에도 제공하도록 선택할 수 있습니다(MAY).

이 명세는 의존성 그룹과 이름이 같은 엑스트라를 두는 것을 금지하지 않는다는 점에 유의하십시오.

사용자는 엑스트라와 이름이 같은 의존성 그룹을 만들지 않는 것이 좋습니다. 도구는 이렇게 이름이 일치하는 경우를 오류로 취급할 수 있습니다(MAY).

검증과 호환성

의존성 그룹을 지원하는 도구는 데이터를 사용하기 전에 검증하고 싶을 수 있습니다. 그러나 이러한 검증 동작을 구현하는 도구는 새로운 문법이 있을 때 불필요하게 오류나 경고를 내지 않도록, 이 명세가 앞으로 확장될 여지를 남겨 두는 데 주의해야 합니다.

도구는 의존성 그룹에서 인식할 수 없는 데이터를 평가하거나 처리할 때 오류를 내야 합니다(SHOULD).

도구는 모든 의존성 그룹의 리스트 내용을 미리 검증해서는 안 됩니다(SHOULD NOT).

즉, 다음과 같은 데이터가 있을 때 대부분의 도구는 foo 그룹의 사용을 허용하고, bar 그룹이 사용될 때만 오류를 냅니다:

[dependency-groups]
foo = ["pyparsing"]
bar = [{set-phasers-to = "stun"}]

더 엄격할 수 있는 린터와 검증기

주로 의존성 그룹을 설치하거나 해석하는 도구에는 사전 검증을 권장하지 않습니다. 린터와 검증 도구에는 이 권고를 무시할 만한 타당한 이유가 있을 수 있습니다.

참조 구현

다음 참조 구현은 의존성 그룹의 내용을 줄바꿈으로 구분하여 stdout에 출력합니다. 따라서 그 출력은 유효한 requirements.txt 데이터입니다.

import re
import sys
import tomllib
from collections import defaultdict

from packaging.requirements import Requirement


def _normalize_name(name: str) -> str:
    return re.sub(r"[-_.]+", "-", name).lower()


def _normalize_group_names(dependency_groups: dict) -> dict:
    original_names = defaultdict(list)
    normalized_groups = {}

    for group_name, value in dependency_groups.items():
        normed_group_name = _normalize_name(group_name)
        original_names[normed_group_name].append(group_name)
        normalized_groups[normed_group_name] = value

    errors = []
    for normed_name, names in original_names.items():
        if len(names) > 1:
            errors.append(f"{normed_name} ({', '.join(names)})")
    if errors:
        raise ValueError(f"Duplicate dependency group names: {', '.join(errors)}")

    return normalized_groups


def _resolve_dependency_group(
    dependency_groups: dict, group: str, past_groups: tuple[str, ...] = ()
) -> list[str]:
    if group in past_groups:
        raise ValueError(f"Cyclic dependency group include: {group} -> {past_groups}")

    if group not in dependency_groups:
        raise LookupError(f"Dependency group '{group}' not found")

    raw_group = dependency_groups[group]
    if not isinstance(raw_group, list):
        raise ValueError(f"Dependency group '{group}' is not a list")

    realized_group = []
    for item in raw_group:
        if isinstance(item, str):
            # packaging.requirements.Requirement parsing ensures that this is a valid
            # PEP 508 Dependency Specifier
            # raises InvalidRequirement on failure
            Requirement(item)
            realized_group.append(item)
        elif isinstance(item, dict):
            if tuple(item.keys()) != ("include-group",):
                raise ValueError(f"Invalid dependency group item: {item}")

            include_group = _normalize_name(next(iter(item.values())))
            realized_group.extend(
                _resolve_dependency_group(
                    dependency_groups, include_group, past_groups + (group,)
                )
            )
        else:
            raise ValueError(f"Invalid dependency group item: {item}")

    return realized_group


def resolve(dependency_groups: dict, group: str) -> list[str]:
    if not isinstance(dependency_groups, dict):
        raise TypeError("Dependency Groups table is not a dict")
    if not isinstance(group, str):
        raise TypeError("Dependency group name is not a str")
    return _resolve_dependency_group(dependency_groups, group)


if __name__ == "__main__":
    with open("pyproject.toml", "rb") as fp:
        pyproject = tomllib.load(fp)

    dependency_groups_raw = pyproject["dependency-groups"]
    dependency_groups = _normalize_group_names(dependency_groups_raw)
    print("\n".join(resolve(pyproject["dependency-groups"], sys.argv[1])))

하위 호환성

이 글을 쓰는 시점에 pyproject.toml 파일 안의 dependency-groups 네임스페이스는 사용되지 않고 있습니다. 최상위 네임스페이스는 packaging.python.org에 명시된 표준만 사용할 수 있도록 예약되어 있으므로, 직접적인 하위 호환성 문제는 없습니다.

그러나 이 기능의 도입은 여러 생태계 도구, 특히 setup.py와 requirements.txt의 데이터 검사를 지원하려는 도구에 영향을 미칩니다.

감사 및 업데이트 도구

매우 다양한 도구가 requirements.txt 파일로 표현된 Python 의존성 데이터를 이해합니다. (예: Dependabot, Tidelift 등)

이러한 도구는 의존성 데이터를 검사하며, 경우에 따라 도구 지원 업데이트나 완전 자동화된 업데이트를 제공합니다. 처음에는 이러한 도구 중 어느 것도 새로운 의존성 그룹을 지원하지 않을 것으로 예상하며, 생태계 전반의 지원이 갖춰지기까지는 여러 달, 심지어 몇 년이 걸릴 수도 있습니다.

그 결과, 의존성 그룹 사용자는 의존성 그룹을 사용하기 시작하는 시점에 워크플로와 도구 지원이 저하되는 것을 겪게 됩니다. 이는 의존성 데이터를 어디에 어떻게 인코딩할지에 관한 모든 새로운 표준에 해당하는 사실입니다.

보안 영향

이 PEP는 프로젝트의 의존성 정보를 명시하기 위한 새로운 문법과 데이터 형식을 도입합니다. 그러나 의존성을 처리하거나 해석하는 메커니즘을 새로 명시하여 도입하지는 않습니다.

따라서 이 기능은 의존성을 설치하는 데 이미 쓰이고 있을 수 있는 도구들이 원래 지닌 보안 우려 외에 다른 보안 우려를 일으키지 않습니다. 즉, requirements.txt 파일에 악성 의존성을 지정할 수 있는 것과 마찬가지로 이곳에도 악성 의존성을 지정할 수 있습니다.

교육 방법

이 기능은 정식 명칭인 “의존성 그룹(Dependency Groups)”으로 불러야 합니다.

기본 사용 형태는 일반적인 requirements.txt 데이터의 변형으로 가르쳐야 합니다. 표준 의존성 지정자(PEP 508)를 이름이 붙은 목록에 추가할 수 있습니다. pip에 requirements.txt 파일에서 설치하도록 요청하는 대신, pip 또는 관련 워크플로 도구가 이름이 붙은 의존성 그룹에서 설치합니다.

Python을 처음 사용하는 사용자에게는 현재 requirements.txt 파일 사용법을 가르치는 것과 비슷하게, 의존성 그룹을 담은 섹션을 pyproject.toml에 만드는 방법을 곧바로 가르칠 수 있습니다. 이를 통해 Python을 처음 사용하는 사용자는 패키지 빌드를 배우지 않고도 pyproject.toml 파일에 대해 배울 수 있습니다. 다른 테이블 없이 [dependency-groups]만 있는 pyproject.toml 파일도 유효합니다.

신규 사용자와 숙련된 사용자 모두에게 의존성 그룹 포함(Dependency Group Includes)을 설명해야 합니다. requirements.txt 사용 경험이 있는 사용자에게는 이를 -r에 대응하는 기능으로 설명할 수 있습니다. 신규 사용자에게는 포함을 사용하면 한 의존성 그룹이 다른 의존성 그룹을 확장할 수 있다고 가르쳐야 합니다. 비슷한 설정 인터페이스나 Python의 list.extend 메서드에 빗대어 이 개념을 설명할 수 있습니다.

setup.py 패키징을 사용해 본 Python 사용자는 pyproject.toml 이전부터 쓰이던, 패키지 메타데이터를 동적으로 정의하는 일반적인 관행에 익숙할 수 있습니다. requirements.txt 파일에서 불러온 요구 사항이나 setup() 호출 전에 정의한 정적 목록은 의존성 그룹에 쉽게 빗댈 수 있습니다.

의존성 그룹 사용을 위한 인터페이스

이 명세는 project 테이블을 통해 빌드된 패키지에 포함하는 것 외에는 의존성 그룹을 다루기 위한 범용 인터페이스를 제공하지 않습니다. 이는 도구 작성자와 사용자 모두에게 영향을 미칩니다.

도구 작성자는 의존성 그룹이 자신의 사용자 스토리와 관련이 있는지, 있다면 어떻게 관련되는지를 판단하고 그에 맞는 자체 인터페이스를 구축해야 합니다. 환경 관리자, 리졸버, 설치 프로그램 및 관련된 비빌드 도구는 “PEP 735 의존성 그룹”을 지원한다고 문서화할 수 있지만, 그 사용 방식을 문서화할 책임은 각 도구에 있습니다. 빌드 백엔드가 의존성 그룹을 지원하려면 project 테이블에서의 포함을 지원해야 하지만, 그 밖의 엄격한 요구 사항은 없습니다.

사용자에게 미치는 주된 영향은 패키지 빌드 외의 용도로 의존성 그룹을 사용하려 할 때마다 관련 도구의 문서를 참고해야 한다는 점입니다. 도구는 문서나 런타임 경고 또는 오류를 통해, 권장되지 않거나 지원되지 않는 사용 방식을 사용자에게 알려야 합니다. 예를 들어 어떤 도구가 모든 의존성 그룹이 서로 호환되어 모순되는 패키지 지정자를 포함하지 않도록 요구하려 한다면, 그 도구는 해당 제약을 문서화하고 사용자가 그 도구의 목적에 맞게 의존성 그룹을 적절히 활용하는 방법을 안내해야 합니다.

기각된 아이디어

각 의존성 그룹을 테이블로 정의하지 않는 이유는 무엇입니까?

향후 확장을 허용하는 것이 목표라면, 각 의존성 그룹을 하위 테이블로 정의하여 앞으로 각 그룹에 키를 추가할 수 있게 하는 방식이 향후 가장 큰 유연성을 제공합니다.

그러나 이 방식은 구조를 더 깊게 중첩시키므로 가르치고 배우기가 더 어려워집니다. 이 PEP의 목표 중 하나는 여러 requirements.txt 사용 사례를 쉽게 대체하는 것입니다.

의존성 지정자를 확장하는 특별한 문자열 문법을 정의하지 않는 이유는 무엇입니까?

이 명세의 초기 초안에서는 의존성 그룹 포함과 경로 의존성(Path Dependencies)을 위한 구문 형식을 정의했습니다.

그러나 이 접근 방식에는 세 가지 주요 문제가 있었습니다:

  • PEP 508 이상으로, 가르쳐야 하는 문자열 문법을 복잡하게 만듭니다
  • 결과 문자열을 항상 PEP 508 지정자와 구별해야 하므로 구현이 복잡해집니다

PEP 508이 아닌 의존성 지정자를 더 허용하지 않는 이유는 무엇입니까?

논의 과정에서 PEP 508로 가능한 것보다 표현력이 더 높은 지정자가 필요한 여러 사용 사례가 제기되었습니다.

로컬 경로를 가리키는 “경로 의존성(Path Dependencies)”과 [project.dependencies]에 대한 참조가 특히 관심을 받았습니다.

그러나 이러한 기능에 대한 기존 표준은 없습니다(pip의 구현 세부 사항이라는 사실상의 표준은 제외합니다).

따라서 이러한 기능을 이 PEP에 포함하려 하면 이 다양한 기능과 pip의 동작까지 표준화해야 하므로 범위가 크게 늘어납니다.

pip install -e와 PEP 660으로 표현되는 편집 가능 설치(editable installation)의 표기 방식을 표준화하는 데 특히 많은 노력을 기울였습니다. 그러나 편집 가능 설치의 생성은 빌드 백엔드에 대해 표준화되어 있지만, 편집 가능 설치의 동작은 설치 프로그램에 대해 표준화되어 있지 않습니다. 이 PEP에 편집 가능 설치를 포함하려면 이를 지원하는 모든 도구가 편집 가능 설치를 할 수 있어야 합니다.

따라서 Poetry와 PDM이 이러한 기능 중 일부에 대한 문법을 제공하기는 하지만, 현재로서는 의존성 그룹에 포함하기에 충분히 표준화되지 않은 것으로 간주합니다.

테이블 이름을 [run], [project.dependency-groups] 등으로 하지 않은 이유는 무엇입니까?

이 개념에 붙일 수 있는 이름은 많습니다. 이 테이블은 이미 존재하는 [project.dependencies] 및 [project.optional-dependencies] 테이블은 물론, 경우에 따라 새로운 [external] 의존성 테이블과도 함께 존재해야 합니다(작성 시점 기준으로 [external] 테이블을 정의하는 PEP 725는 진행 중입니다).

[run]은 이전 논의에서 유력한 제안이었지만, 제안된 용도는 단일 런타임 의존성 집합을 중심으로 했습니다. 이 PEP는 여러 의존성 그룹을 명시적으로 규정하므로 [run]은 덜 적합합니다. 이는 특정 런타임 컨텍스트 하나만을 위한 의존성 데이터가 아니라 여러 컨텍스트를 위한 것입니다.

[project.dependency-groups]는 [project.dependencies] 및 [project.optional-dependencies]와 좋은 대응을 이루겠지만, 패키지가 아닌 프로젝트에는 큰 단점이 있습니다. [project]는 name과 version 같은 여러 키를 정의해야 합니다. 이 이름을 사용하면 이러한 키가 없어도 되도록 [project] 테이블을 재정의하거나, 패키지가 아닌 프로젝트에 이러한 키를 정의하고 사용하도록 요구해야 합니다. 더 나아가 이는 사실상 패키지가 아닌 모든 프로젝트에 스스로를 패키지로 취급하도록 허용할 것을 요구하는 셈입니다.

pip에서 계획 중인 --only-deps 구현으로 충분하지 않은 이유는 무엇입니까?

pip는 현재 –only-deps 플래그를 추가하는 기능을 로드맵에 두고 있습니다. 이 플래그는 사용자가 현재 패키지를 설치하지 않고도 패키지 의존성과 엑스트라를 설치할 수 있게 하려는 것입니다.

이 플래그는 패키지가 아닌 프로젝트의 요구를 해결하지 못하며, 패키지 의존성 없이 엑스트라만 설치할 수도 없습니다.

<환경 관리자>가 해결책이 되지 않는 이유는 무엇입니까?

tox, Nox, Hatch 같은 기존 환경 관리자는 이미 설정 데이터의 일부로 인라인 의존성을 나열하는 기능을 갖추고 있습니다. 이는 많은 개발 의존성 요구를 충족하며, 의존성 그룹을 실행할 수 있는 관련 작업과 명확하게 연결합니다. 이러한 메커니즘은 좋지만 충분하지는 않습니다.

첫째, 이러한 메커니즘은 패키지가 아닌 프로젝트의 요구를 해결하지 못합니다.

둘째, 다른 도구가 이 데이터에 접근하는 데 사용할 표준이 없습니다. 이는 IDE나 Dependabot 같은 상위 수준 도구에 영향을 주며, 이러한 도구는 의존성 그룹과 깊이 통합될 수 없습니다. (예를 들어, 이 글을 쓰는 시점에 Dependabot은 tox.ini 파일에 고정된 의존성에 플래그를 표시하지 않습니다.)

보류된 아이디어

[project.dependencies]나 [project.optional-dependencies]에서 의존성 그룹 포함(Dependency Group Includes)을 지원하지 않는 이유는 무엇입니까?

이 명세의 초기 초안에서는 [project] 테이블에서 의존성 그룹 포함(Dependency Group Includes)을 사용할 수 있도록 허용했습니다. 그러나 커뮤니티 피드백 과정에서 여러 문제가 제기되었고, 그 결과 이 기능은 제거되었습니다.

의존성 그룹 포함을 허용해도 추가로 해결되는 사용 사례는 소수에 불과했지만, 명세의 범위는 크게 넓어졌습니다. 특히 이 기능을 포함하면 이 추가 사항의 영향을 받는 당사자의 수가 늘어납니다. 빌드 백엔드, SBOM 생성기, 의존성 분석기를 비롯하여 [project] 테이블을 읽는 많은 도구는 [project]가 변경되면 영향을 받지만, 새로운 (그러나 서로 연결되지 않은) [dependency-groups] 테이블이 있을 때는 지금처럼 계속 동작할 수 있습니다.

위의 우려와는 별개로, [project] 테이블에서 의존성 그룹을 포함할 수 있게 하면 패키지 유지 관리자가 의존성 메타데이터를 현재의 표준 위치 밖으로 옮기도록 부추기게 됩니다. 이는 정적 pyproject.toml 메타데이터를 복잡하게 만들며, 의존성 메타데이터를 한 곳에 저장한다는 PEP 621의 목표와도 충돌합니다.

끝으로, 이 PEP에서 [project] 지원을 제외한 것이 최종 결정은 아닙니다. 해당 테이블에서 포함을 사용하는 방식이나 [dependency-groups]에서 [project]로 포함하는 문법은 향후 PEP에서 도입되어 그 자체의 장단점에 따라 검토될 수 있습니다.

[project]에서의 의존성 그룹 포함 사용 사례

이 PEP에서는 보류되었지만, [project] 테이블에서 포함을 허용하면 여러 사용 사례를 해결할 수 있습니다.

특히 패키지 개발자가 패키지 자체는 제외하고 패키지의 의존성만 설치하려는 경우가 있습니다.

예를 들면 다음과 같습니다.

  • 의존성을 빌드할 때와 패키지 자체를 빌드할 때 서로 다른 환경 변수나 옵션을 지정하는 경우
  • 의존성이 설치 대상 패키지와 분리된 계층형 컨테이너 이미지를 만드는 경우
  • 패키지 자체를 빌드하고 설치하지 않고도 분석 환경(예: 타입 검사)에 의존성을 제공하는 경우

마지막 경우의 예로, 다음 pyproject.toml 예시를 살펴보십시오.

[project]
dependencies = [{include = "runtime"}]
[optional-dependencies]
foo = [{include = "foo"}]
[dependency-groups]
runtime = ["a", "b"]
foo = ["c", "d"]
typing = ["mypy", {include = "runtime"}, {include = "foo"}]

이 경우 패키지의 모든 런타임 의존성을 포함하되 패키지 자체는 포함하지 않는 typing 그룹을 정의할 수 있습니다. 이를 통해 typing 의존성 그룹을 사용할 때 패키지 설치를 건너뛸 수 있습니다. 이는 더 효율적일 뿐만 아니라 테스트 시스템에 대한 요구 사항도 줄여 줄 수 있습니다.

[build-system.requires]에서 의존성 그룹 포함을 지원하지 않는 이유는 무엇입니까?

[project]에서 의존성 그룹을 사용하는 것은 허용하지 않으므로, [build-system.requires]는 [project.dependencies]와 비교하여 검토할 수 있습니다.

그룹에 지정된 빌드 요구 사항은 패키지 요구 사항보다 이론적으로 활용될 여지가 적습니다. 또한 이러한 변경은 PEP 517 프런트엔드에도 영향을 미치며, 프런트엔드는 빌드 환경을 준비하기 위해 의존성 그룹을 지원해야 합니다.

[project.dependencies] 및 [project.optional-dependencies]를 변경하는 것과 비교하면, [build-system.requires]의 동작을 변경하는 것은 영향이 더 크고 잠재적인 활용처는 더 적습니다. 따라서 이 PEP가 [project] 테이블을 변경하지 않기로 한 만큼, [build-system]의 변경도 보류합니다.

현재 프로젝트를 포함하는 의존성 그룹을 지원하지 않는 이유는 무엇입니까?

의존성 그룹의 여러 사용 시나리오는 [project] 테이블에 정의된 패키지와 함께 의존성 그룹을 설치하는 것을 중심으로 합니다. 예를 들어 패키지를 테스트하려면 테스트 의존성과 패키지 자체를 설치해야 합니다. 또한 의존성 그룹이 주 패키지와 호환되는지는 잠금 파일 생성기에 유용한 입력 정보가 됩니다.

이러한 경우에는 의존성 그룹이 프로젝트 자체에 의존한다고 선언할 수 있는 것이 바람직합니다. 논의 과정에서 나온 문법 예시로는 {include-project = true}와 {include-group = ":project:"}가 있었습니다.

그러나 경로 의존성(Path Dependencies)으로 PEP 508을 확장하는 명세가 확립되면, 의존성 그룹(Dependency Groups)에서 주 패키지를 지정하는 방법이 두 가지가 됩니다. 예를 들어 .이 공식적으로 지원되고 {include-project = true}가 이 PEP에 포함된다면, 의존성 그룹은 다음 그룹 중 어느 것이든 지정할 수 있습니다

[dependency-groups]
case1 = [{include-project = true}]
case2 = ["."]
case3 = [{include-project = true}, "."]
case4 = [{include-project = false}, "."]

여러 가지 다른 옵션이 pyproject.toml에 정의된 패키지를 지정하는 혼란스러운 미래를 피하기 위해, 이 관계를 선언하는 문법은 이 PEP에서 제외합니다.

부록 A: Python 이외 언어의 선행 사례

이 절은 주로 정보 제공을 목적으로 하며, 다른 언어 생태계가 비슷한 문제를 어떻게 해결하는지 기록합니다.

JavaScript와 package.json

JavaScript 커뮤니티에서 패키지는 pyproject.toml과 범위가 비슷한 정식 설정 및 데이터 파일을 package.json에 둡니다.

package.json에서는 "dependencies"와 "devDependencies" 두 키가 의존성 데이터를 제어합니다. "dependencies"의 역할은 사실상 pyproject.toml의 [project.dependencies]와 같으며, 패키지의 직접 의존성을 선언합니다.

"dependencies" 데이터

의존성 데이터는 package.json에 패키지 이름에서 버전 지정자로의 매핑으로 선언됩니다.

버전 지정자는 Python의 PEP 440 버전 지정자와 유사하게, 가능한 버전, 범위 및 기타 값을 표현하는 작은 문법을 지원합니다.

예를 들어, 다음은 몇 가지 의존성을 선언하는 package.json 파일의 일부입니다:

{
    "dependencies": {
        "@angular/compiler": "^17.0.2",
        "camelcase": "8.0.0",
        "diff": ">=5.1.0 <6.0.0"
    }
}

@ 기호는 조직이 소유한 패키지에 대해 패키지 소유자를 선언하는 스코프입니다. 따라서 "@angular/compiler"는 angular 소유로 묶인 compiler라는 이름의 패키지를 선언합니다.

URL과 로컬 경로를 참조하는 의존성

의존성 지정자는 Python 패키징의 규정과 유사하게 URL과 Git 저장소를 위한 문법을 지원합니다.

버전 번호 대신 URL을 사용할 수 있습니다. URL을 사용하면 암묵적으로 패키지 소스 코드의 tarball을 가리킵니다.

Git 저장소도 마찬가지로 사용할 수 있으며, committish 지정자도 지원합니다.

PEP 440과 달리, NPM은 의존성에 패키지 소스 코드 디렉터리의 로컬 경로를 사용하는 것을 허용합니다. 표준 npm install --save 명령으로 이러한 데이터를 package.json에 추가하면, 경로는 package.json이 있는 디렉터리를 기준으로 한 상대 경로로 정규화되고 앞에 file:이 붙습니다. 예를 들어, 다음 package.json 일부에는 현재 디렉터리와 같은 수준에 있는 디렉터리에 대한 참조가 들어 있습니다:

{
    "dependencies": {
        "my-package": "file:../foo"
    }
}

공식 NPM 문서는 로컬 경로 의존성을 공개 패키지 저장소에 게시해서는 “안 된다(should not)”고 명시하지만, 게시된 패키지에 포함된 그러한 의존성 데이터가 본질적으로 유효한지 아닌지에 대해서는 아무런 언급도 하지 않습니다.

"devDependencies" 데이터

package.json에는 "dependencies"와 같은 형식으로 "devDependencies"라는 두 번째 섹션을 둘 수 있습니다. "devDependencies"에 선언된 의존성은 패키지 저장소에서 패키지를 설치할 때(예: 의존성 해석 과정의 일부로)는 기본적으로 설치되지 않지만, package.json이 있는 소스 트리에서 npm install을 실행하면 설치됩니다.

"dependencies"가 URL과 로컬 경로를 지원하는 것처럼 "devDependencies"도 이를 지원합니다.

"peerDependencies"와 "optionalDependencies"

package.json에는 관련이 있는 두 개의 추가 섹션이 있습니다.

"peerDependencies"는 "dependencies"와 같은 형식으로 의존성 목록을 선언하지만, 이 목록은 호환성 선언이라는 의미를 가집니다. 예를 들어, 다음 데이터는 foo 패키지 버전 2와의 호환성을 선언합니다:

{
    "peerDependencies": {
        "foo": "2.x"
    }
}

"optionalDependencies"는 가능하면 설치해야 하지만 사용할 수 없더라도 실패로 처리해서는 안 되는 의존성 목록을 선언합니다. 이것 역시 "dependencies"와 같은 매핑 형식을 사용합니다.

"peerDependenciesMeta"

"peerDependenciesMeta"는 "peerDependencies"를 처리하는 방식을 추가로 제어할 수 있게 하는 섹션입니다.

다음 예시처럼 이 섹션에서 패키지를 optional로 설정하면 누락된 의존성에 대한 경고를 끌 수 있습니다:

{
    "peerDependencies": {
        "foo": "2.x"
    },
    "peerDependenciesMeta": {
        "foo": {
            "optional": true
        }
    }
}

--omit과 --include

npm install 명령은 --omit과 --include라는 두 가지 옵션을 지원하며, 이 옵션으로 “prod”, “dev”, “optional”, “peer” 의존성의 설치 여부를 제어할 수 있습니다.

“prod”라는 이름은 "dependencies" 아래에 나열된 의존성을 가리킵니다.

기본적으로 소스 트리에서 npm install을 실행하면 네 그룹이 모두 설치되지만, 이 옵션들을 사용하면 설치 동작을 더 정밀하게 제어할 수 있습니다. 또한 이 값들은 .npmrc 파일에 선언할 수 있으므로 사용자별 설정과 프로젝트별 설정으로 설치 동작을 제어할 수 있습니다.

Ruby와 Ruby Gems

Ruby 프로젝트는 Ruby 생태계의 패키지(“gem”)를 만들기 위한 것일 수도 있고 아닐 수도 있습니다. 실제로 이 언어 사용자 대부분은 gem을 만들려 하지 않으며 자신만의 패키지를 만드는 데 관심이 없을 것으로 예상됩니다. 많은 튜토리얼이 패키지를 만드는 방법을 다루지 않으며, 도구 체인은 지원되는 사용 사례에서 사용자 코드의 패키징을 결코 요구하지 않습니다.

Ruby는 요구 사항 명세를 별개의 파일 두 개로 나눕니다.

  • Gemfile: 의존성 그룹 형태의 요구 사항 데이터만 지원하는 전용 파일
  • <package>.gemspec: 패키지(gem) 메타데이터를 선언하기 위한 전용 파일

bundle 명령을 제공하는 bundler 도구는 Gemfile 데이터를 사용하는 주요 인터페이스입니다.

gem 도구는 gem build 명령을 통해 .gemspec 데이터로 gem을 빌드하는 역할을 맡습니다.

Gemfile과 bundle

Gemfile은 임의 개수의 group 선언으로 둘러싸인 gem 지시문을 담고 있는 Ruby 파일입니다. gem 지시문은 group 선언 밖에서도 사용할 수 있으며, 이 경우에는 이름 없는 의존성 그룹을 암묵적으로 이룹니다.

예를 들어 다음 Gemfile은 rails를 프로젝트 의존성으로 나열합니다. 그 밖의 모든 의존성은 그룹 아래에 나열됩니다:

source 'https://rubygems.org'

gem 'rails'

group :test do
  gem 'rspec'
end

group :lint do
  gem 'rubocop'
end

group :docs do
  gem 'kramdown'
  gem 'nokogiri'
end

사용자가 이 데이터를 가지고 bundle install을 실행하면 모든 그룹이 설치됩니다. 사용자는 .bundle/config에 있는 bundler 설정을 직접 또는 CLI로 만들거나 수정하여 그룹을 선택 해제할 수 있습니다. 예를 들면 bundle config set --local without 'lint:docs'와 같습니다.

위 데이터로는 최상위 수준에서 사용한 'rails' gem을 제외하거나 그 암묵적 그룹을 이름으로 참조할 수 없습니다.

gemspec과 패키지에 담긴 의존성 데이터

gemspec 파일은 Gem::Specification 인스턴스 선언을 담고 있는 Ruby 파일입니다.

Gem::Specification의 필드 중 패키지 의존성 데이터와 관련된 것은 두 개뿐입니다. 바로 add_development_dependency와 add_runtime_dependency입니다. Gem::Specification 객체는 런타임 의존성을 추가하는 add_dependency를 비롯해 의존성을 동적으로 추가하는 메서드도 제공합니다.

다음은 단순화를 위해 많은 필드를 제거하거나 줄인 rails.gemspec 파일의 변형입니다:

version = '7.1.2'

Gem::Specification.new do |s|
  s.platform    = Gem::Platform::RUBY
  s.name        = "rails"
  s.version     = version
  s.summary     = "Full-stack web application framework."

  s.license = "MIT"
  s.author   = "David Heinemeier Hansson"

  s.files = ["README.md", "MIT-LICENSE"]

  # shortened from the real 'rails' project
  s.add_dependency "activesupport", version
  s.add_dependency "activerecord",  version
  s.add_dependency "actionmailer",  version
  s.add_dependency "activestorage", version
  s.add_dependency "railties",      version
end

add_development_dependency를 사용하지 않는다는 점에 유의하십시오. 다른 몇몇 주류 대형 패키지(예: rubocop)도 자신의 gem에서 개발 의존성을 사용하지 않습니다.

반면 다른 프로젝트들은 이 기능을 실제로 사용합니다. 예를 들어, kramdown은 개발 의존성을 활용하며, Rakefile에 다음과 같은 명세를 포함하고 있습니다:

s.add_dependency "rexml"
s.add_development_dependency 'minitest', '~> 5.0'
s.add_development_dependency 'rouge', '~> 3.0', '>= 3.26.0'
s.add_development_dependency 'stringex', '~> 1.5.1'

개발 의존성의 목적은 .gemspec의 일부로서 암묵적인 그룹을 선언하는 것뿐이며, 이 그룹은 이후 bundler가 사용할 수 있습니다.

자세한 내용은 bundler의 Gemfile 문서에서 gemspec 지시어 항목을 참고하십시오. 그러나 .gemspec 개발 의존성과 Gemfile/bundle 사용 간의 연동은 예시를 통해 가장 잘 이해할 수 있습니다.

gemspec 개발 의존성 예시

Gemfile과 .gemspec으로 구성된 다음과 같은 간단한 프로젝트를 생각해 봅시다. cool-gem.gemspec 파일:

Gem::Specification.new do |s|
  s.author = 'Stephen Rosen'
  s.name = 'cool-gem'
  s.version = '0.0.1'
  s.summary = 'A very cool gem that does cool stuff'
  s.license = 'MIT'

  s.files = []

  s.add_dependency 'rails'
  s.add_development_dependency 'kramdown'
end

그리고 Gemfile:

source 'https://rubygems.org'

gemspec

Gemfile의 gemspec 지시어는 로컬에 있는 cool-gem.gemspec 파일에 정의된 로컬 패키지 cool-gem에 대한 의존성을 선언합니다. 이 지시어는 또한 모든 개발 의존성을 이름이 development인 의존성 그룹에 암묵적으로 추가합니다.

따라서 이 경우 gemspec 지시어는 다음 Gemfile 내용과 동등합니다:

gem 'cool-gem', :path => '.'

group :development do
  gem 'kramdown'
end

부록 B: Python의 선행 사례

의존성 그룹에 대한 기존 표준이 없는 상황에서, 잘 알려진 두 워크플로 도구인 PDM과 Poetry는 각자의 해결책을 정의했습니다.

이 절에서는 Python에서 의존성 그룹을 정의하고 사용하는 방식에 관한 선행 사례로서 주로 이 두 도구를 다룹니다.

프로젝트는 패키지입니다

PDM과 Poetry는 모두 자신이 지원하는 프로젝트를 패키지로 취급합니다. 이를 통해 두 도구는 필요한 일부 작업에 표준 pyproject.toml 메타데이터를 사용하고 상호 작용할 수 있으며, 자체 빌드 백엔드로 빌드와 설치를 수행하여 “현재 프로젝트”의 설치를 지원할 수 있습니다.

사실상 이는 Poetry와 PDM 모두 패키지가 아닌 프로젝트를 지원하지 않는다는 뜻입니다.

비표준 의존성 지정자

PDM과 Poetry는 어떤 공유 표준에도 속하지 않는 추가 기능으로 PEP 508 의존성 지정자를 확장합니다. 다만 두 도구는 이러한 문제에 대해 약간 다른 접근 방식을 사용합니다.

PDM은 pip install에 전달하는 인자 목록처럼 보이는 문법을 통해 로컬 경로 지정과 편집 가능(editable) 설치를 지원합니다. 예를 들어, 다음 의존성 그룹은 로컬 패키지를 편집 가능 모드로 포함합니다:

[tool.pdm.dev-dependencies]
mygroup = ["-e file:///${PROJECT_ROOT}/foo"]

이는 foo 디렉터리로부터의 로컬 편집 가능 설치를 포함하는 의존성 그룹 mygroup을 선언합니다.

Poetry는 의존성 그룹을 패키지 이름을 지정자에 매핑하는 테이블로 기술합니다. 예를 들어, 위의 mygroup 예시와 동일한 구성은 Poetry에서 다음과 같이 나타낼 수 있습니다:

[tool.poetry.group.mygroup]
foo = { path = "foo", editable = true }

PDM은 문자열 문법만 사용하는 반면, Poetry는 의존성을 기술하는 테이블을 도입합니다.

의존성 그룹 설치 및 참조

PDM과 Poetry는 모두 의존성 그룹 설치를 각 도구 고유의 방식으로 지원합니다. 두 프로젝트 모두 자체 잠금 파일 형식을 지원하므로, 의존성 그룹 이름을 사용하여 해당 그룹의 잠긴 의존성 데이터를 투명하게 참조하는 기능도 둘 다 갖추고 있습니다.

그러나 두 도구의 의존성 그룹 모두 tox, nox, pip와 같은 다른 도구에서 네이티브로 참조할 수 없습니다. 예를 들어 tox에서 의존성 그룹을 설치하려면 PDM이나 Poetry를 명시적으로 호출하여 해당 도구의 의존성 데이터를 파싱하고 관련 설치 단계를 수행해야 합니다.

부록 C: 사용 사례

웹 애플리케이션

웹 애플리케이션(예: Django 또는 Flask 앱)은 배포물을 빌드할 필요가 없는 경우가 많으며, 대신 소스를 묶어 배포 툴체인으로 보냅니다.

예를 들어 소스 코드 저장소는 Python 패키징 메타데이터와 함께 컨테이너화 또는 기타 빌드 파이프라인 메타데이터(Dockerfile 등)를 정의할 수 있습니다. Python 애플리케이션은 저장소 전체를 빌드 컨텍스트에 복사하고, 의존성을 설치한 다음, 그 결과를 머신 이미지나 컨테이너로 묶는 방식으로 빌드됩니다.

이러한 애플리케이션에는 빌드를 위한 의존성 그룹뿐 아니라 린트, 테스트 등을 위한 의존성 그룹도 있습니다. 실제로 오늘날 이러한 애플리케이션은 패키징 도구와 extras 같은 메커니즘으로 의존성 그룹을 관리하기 위해 스스로를 패키지로 정의하는 경우가 많습니다. 그러나 이들은 개념적으로 sdist나 wheel 형식으로 배포하기 위한 패키지가 아닙니다.

의존성 그룹을 사용하면 이러한 애플리케이션은 패키징 메타데이터에 의존하지 않고, 또 자신의 요구 사항을 패키징 용어로 표현하려 애쓰지 않고도 다양한 의존성을 정의할 수 있습니다.

라이브러리

라이브러리는 배포물(sdist 및 wheel)을 빌드하여 PyPI에 게시하는 Python 패키지입니다.

라이브러리에서 의존성 그룹은 개발 의존성 그룹을 정의하는 데 extras를 대신할 수 있는 수단이며, 앞서 언급한 중요한 장점을 지닙니다.

라이브러리는 테스트와 타입 검사를 위한 test 및 typing 그룹을 정의할 수 있으며, 이 그룹들은 ([project.dependencies]에 지정된) 라이브러리 자체의 의존성에 의존합니다.

다른 개발 용도에서는 패키지를 전혀 설치할 필요가 없을 수도 있습니다. 예를 들어 lint 의존성 그룹은 black, ruff, flake8 같은 도구만 설치하므로, 라이브러리 없이 설치해도 유효하며 설치 속도도 더 빠를 수 있습니다.

lint와 test 환경은 IDE나 편집기 지원을 훅으로 연결하기에도 유용한 위치가 될 수 있습니다. 이러한 사용법에 대한 자세한 설명은 아래 사례를 참조하십시오.

다음은 라이브러리에 적합할 수 있는 의존성 그룹 테이블의 예입니다:

[dependency-groups]
test = ["pytest<8", "coverage"]
typing = ["mypy==1.7.1", "types-requests"]
lint = ["black", "flake8"]
typing-test = [{include-group = "typing"}, "pytest<8"]

이 중 어느 것도 라이브러리 자체를 암묵적으로 설치하지 않는다는 점에 유의하십시오. 따라서 test의 경우처럼 필요할 때 적절한 의존성 그룹을 라이브러리와 함께 설치하는 것은 환경 관리 툴체인의 책임입니다.

데이터 과학 프로젝트

데이터 과학 프로젝트는 일반적으로 공통 툴체인을 사용하여 데이터를 처리하고 분석하는 스크립트와 유틸리티의 논리적 모음 형태를 띱니다. 구성 요소는 Jupyter Notebook 형식(ipynb)으로 정의될 수 있지만, 동일한 공통 핵심 유틸리티 집합에 의존합니다.

이러한 프로젝트에는 빌드하거나 설치할 패키지가 없습니다. 따라서 현재 pyproject.toml은 의존성 관리나 선언을 위한 어떠한 해결책도 제공하지 않습니다.

이러한 프로젝트에서는 적어도 하나의 주요 의존성 그룹을 정의할 수 있으면 유용합니다. 예를 들면 다음과 같습니다:

[dependency-groups]
main = ["numpy", "pandas", "matplotlib"]

그러나 여러 스크립트에 추가 지원 도구가 필요할 수도 있습니다. 프로젝트가 시간이 지나며 발전함에 따라, 구성 요소별로 서로 충돌하거나 호환되지 않는 도구 또는 도구 버전을 갖게 될 수도 있습니다.

다음과 같은 더 정교한 구성을 살펴보십시오:

[dependency-groups]
main = ["numpy", "pandas", "matplotlib"]
scikit = [{include-group = "main"}, "scikit-learn==1.3.2"]
scikit-old = [{include-group = "main"}, "scikit-learn==0.24.2"]

이 구성은 scikit과 scikit-old를 공통 의존성 모음의 비슷한 두 변형으로 정의하며, 서로 다른 스크립트에 맞도록 각기 다른 버전의 scikit-learn을 가져옵니다.

이 PEP는 이러한 데이터만 정의합니다. 데이터 과학 프로젝트(또는 다른 어떤 유형의 프로젝트)가 알려진 환경에 의존성을 설치하거나 그 환경을 여러 스크립트와 연결하는 메커니즘은 전혀 공식화하지 않습니다. 이러한 데이터 조합은 도구 작성자가 해결하고, 어쩌면 궁극적으로 표준화할 문제로 남겨 둡니다.

잠금 파일 생성

오늘날 Python 생태계에는 잠금 파일을 생성하는 도구가 여럿 있습니다. PDM과 Poetry는 각자 고유한 잠금 파일 형식을 사용하며, pip-tools는 버전 고정과 해시가 포함된 requirements.txt 파일을 생성합니다.

의존성 그룹에는 필요한 기능 중 많은 것이 없으므로 잠금 파일을 저장하기에 적절한 곳이 아닙니다. 가장 두드러진 점은 의존성 그룹에 해시를 저장할 수 없다는 것인데, 대부분의 잠금 파일 사용자는 해시를 필수로 여깁니다.

그러나 의존성 그룹은 잠금 파일을 생성하는 도구에 유효한 입력입니다. 게다가 PDM과 Poetry는 모두 (각자의 의존성 그룹 개념 안에서) 의존성 그룹 이름을 사용하여 그 그룹의 잠긴 변형을 가리킬 수 있게 합니다.

따라서 잠금 파일을 생성하는 도구를 하나 생각해 보십시오. 여기서는 이 도구를 $TOOL이라고 부르겠습니다. 이 도구는 다음과 같이 사용할 수 있습니다:

$TOOL lock --dependency-group=test
$TOOL install --dependency-group=test --use-locked

이러한 사용 방식을 지원하려면 이 도구는 잠금 파일 데이터에 test라는 이름이 기록되도록 하기만 하면 됩니다.

의존성 그룹 간의 상호 호환성은 보장되지 않습니다. 예를 들어 위의 데이터 과학 예시에는 서로 충돌하는 scikit-learn 버전이 나타납니다. 따라서 잠긴 의존성 그룹 여러 개를 함께 설치하려면 도구가 추가 제약 조건을 적용하거나 추가 잠금 파일 데이터를 생성해야 할 수 있습니다. 이러한 문제는 이 PEP의 범위를 벗어나는 것으로 간주합니다.

조합을 잠그는 방식의 두 가지 예는 다음과 같습니다:

  • 도구는 어떤 조합이든 유효하다고 간주하려면 해당 조합에 대한 잠금 파일 데이터가 명시적으로 생성되어 있어야 한다고 요구할 수 있습니다
  • Poetry는 모든 의존성 그룹이 상호 호환되어야 한다는 요구 사항을 구현하며, 잠긴 버전을 하나만 생성합니다. (즉, 해의 집합이나 행렬이 아니라 단일 해를 찾습니다.)

환경 관리자 입력

tox, Nox, Hatch에서 흔히 쓰는 방식은 테스트 환경에 의존성 집합을 설치하는 것입니다.

예를 들어 tox.ini에서는 타입 검사 의존성을 인라인으로 정의할 수 있습니다:

[testenv:typing]
deps =
    pyright
    useful-types
commands = pyright src/

이러한 조합은 제한된 맥락 안에서 바람직한 개발자 경험을 제공합니다. 해당 환경 관리자에서는 테스트 환경에 필요한 의존성이 그 의존성을 필요로 하는 명령과 함께 선언됩니다. 이러한 의존성은 extras와 달리 패키지 메타데이터에 게시되지 않으며, 해당 환경을 구축하는 데 이 의존성이 필요한 도구가 찾을 수 있습니다.

의존성 그룹은 이러한 요구 사항 데이터를 도구별 위치에서 더 폭넓게 사용할 수 있는 위치로 사실상 “끌어올림”으로써 이러한 사용 방식에 적용됩니다. 위 예시에서는 tox만 선언된 의존성 목록에 접근할 수 있습니다. 의존성 그룹을 지원하는 구현에서는 같은 데이터를 의존성 그룹에서 사용할 수 있을 것입니다:

[dependency-groups]
typing = ["pyright", "useful-types"]

그러면 이 데이터를 여러 도구에서 사용할 수 있습니다. 예를 들어, tox는 위의 deps 사용 방식을 대체하여 dependency_groups = typing과 같은 형태로 지원을 구현할 수 있습니다.

의존성 그룹이 환경 관리자 사용자에게 실질적인 대안이 되려면, 환경 관리자가 인라인 의존성 선언을 지원하는 것과 비슷한 방식으로 의존성 그룹 처리를 지원해야 합니다.

IDE와 편집기의 요구 사항 데이터 사용

IDE 및 편집기 통합 기능은 통합에 사용되는 의존성 그룹에 대해 관례적이거나 설정 가능한 이름 정의가 있으면 도움을 받을 수 있습니다.

편집기나 IDE가 프로젝트의 공개되지 않은 의존성을 찾아낼 수 있으면 유용한 경우가 적어도 두 가지 알려져 있습니다:

  • 테스트: VS Code 같은 IDE는 특정 테스트를 실행하기 위한 GUI 인터페이스를 지원합니다
  • 린팅: 편집기와 IDE는 오류를 강조 표시하거나 자동으로 수정하는 린팅 및 자동 포매팅 통합 기능을 지원하는 경우가 많습니다

이러한 경우는 test, lint, fix 같은 관례적인 그룹 이름을 정의하거나, 의존성 그룹을 선택할 수 있게 하는 설정 메커니즘을 정의하여 처리할 수 있습니다.

예를 들어, 다음 pyproject.toml은 앞서 언급한 세 그룹을 선언합니다:

[dependency-groups]
test = ["pytest", "pytest-timeout"]
lint = ["flake8", "mypy"]
fix = ["black", "isort", "pyupgrade"]

이 PEP는 이러한 이름을 표준화하거나 이러한 용도로 예약하려 하지 않습니다. IDE는 여러 목적에 사용되는 그룹 이름을 표준화할 수도 있고, 사용자가 설정하도록 허용할 수도 있습니다.

이 선언을 통해 프로젝트에 적합한 도구에 대한 프로젝트 작성자의 지식을 그 프로젝트의 모든 편집자와 공유할 수 있습니다.