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

Python 개선 제안 한국어 번역

PEP 722 – 단일 파일 스크립트를 위한 의존성 명세

Author:
Paul Moore <p.f.moore at gmail.com>
PEP-Delegate:
Brett Cannon <brett at python.org>
Discussions-To:
Discourse thread
Status:
Rejected
Type:
Standards Track
Topic:
Packaging
Created:
19-Jul-2023
Post-History:
19-Jul-2023
Superseded-By:
723
Resolution:
21-Oct-2023

Table of Contents

번역·라이선스 안내

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

Warning

This PEP was rejected in favour of PEP 723.

×

초록

이 PEP는 단일 파일 Python 스크립트에 서드파티 의존성을 포함하는 형식을 명시합니다.

동기

모든 Python 코드가 “프로젝트”로 구성되는 것은 아닙니다. 여기서 프로젝트란 pyproject.toml 파일을 갖춘 자체 디렉터리가 있고, 설치 가능한 배포 패키지로 빌드되는 것을 뜻합니다. Python은 스크립팅 언어로도 흔히 사용되며, 이때 Python 스크립트는 셸 스크립트, 배치 파일 등의 (더 나은) 대안이 됩니다. 스크립트를 작성하는 데 쓰일 때 Python 코드는 보통 단일 파일로 저장되며, 이러한 “유틸리티 스크립트” 전용 디렉터리에 두는 경우가 많습니다. 이 디렉터리에는 여러 언어로 된 스크립트가 섞여 있을 수 있으며, Python은 그중 하나일 뿐입니다. 이러한 스크립트는 공유될 수 있으며, 이메일이나 Github gist 같은 URL 링크처럼 아주 간단한 방법으로 공유되는 경우가 많습니다. 하지만 이러한 스크립트는 일반적인 워크플로의 일부로 “배포”되거나 “설치”되는 경우가 보통 없습니다.

이런 식으로 Python을 스크립팅 언어로 사용할 때 생기는 문제 하나는, 스크립트에 필요한 서드파티 의존성이 모두 갖추어진 환경에서 스크립트를 어떻게 실행하느냐입니다. 현재 이 문제를 해결하는 표준 도구는 없으며, 이 PEP도 그러한 도구를 정의하려 하지 않습니다. 그러나 이 문제를 실제로 해결하는 도구라면 어떤 도구든 스크립트에 어떤 서드파티 의존성이 필요한지 알아야 합니다. 이러한 데이터를 저장하는 표준 형식을 정의하면, 기존 도구뿐만 아니라 앞으로 나올 도구도 사용자에게 스크립트에 도구별 메타데이터를 넣도록 요구하지 않고 해당 정보를 얻을 수 있습니다.

근거

핵심 요구 사항은 단일 파일 스크립트를 작성하는 것과 누군가에게 스크립트 사본을 건네는 방식으로 간단히 공유하는 것이므로, 이 PEP는 의존성 데이터를 외부 파일이 아닌 스크립트 자체 안에 포함하는 메커니즘을 정의합니다.

스크립트가 어떤 서드파티 패키지에 의존하는지에 대한 정보를 담는 의존성 블록이라는 개념을 정의합니다.

의존성 블록을 식별하려면 스크립트를 단순히 텍스트 파일로 읽으면 됩니다. 이는 의도된 것입니다. Python 문법은 시간이 지나며 바뀌므로, 스크립트를 Python 코드로 파싱하려면 특정 버전의 Python 문법을 선택해야 하기 때문입니다. 또한 적어도 일부 도구는 Python으로 작성되지 않을 가능성이 높으며, 그러한 도구에 Python 파서를 구현하라고 기대하는 것은 지나친 부담입니다.

그러나 Python 코어를 변경할 필요가 없도록, 이 형식은 Python 파서에게 주석으로 보이도록 설계되었습니다. 의존성 블록이 주석으로 해석되지 않는 코드를 작성할 수도 있지만(예를 들어 Python 여러 줄 문자열 안에 넣는 경우), 그러한 사용은 권장하지 않으며, 일부러 병적인 예시를 만들려는 것이 아니라면 쉽게 피할 수 있습니다.

다른 언어들이 스크립트에서 의존성을 명시할 수 있게 하는 방식에 대한 검토 결과, 이와 같은 “구조화된 주석”이 흔히 쓰이는 접근 방식임을 알 수 있습니다.

명세

이 절의 내용은 Python 패키징 사용자 가이드의 PyPA 명세 절에 “스크립트 파일에 메타데이터 포함하기(Embedding Metadata in Script Files)”라는 제목의 문서로 게시될 예정입니다.

모든 Python 스크립트는 의존성 블록을 포함할 수 있습니다. 의존성 블록은 스크립트를 텍스트 파일로 읽어(즉, 파일을 Python 소스 코드로 파싱하지 않고) 다음 형태의 첫 번째 줄을 찾는 방식으로 식별합니다.:

# Script Dependencies:

해시 문자는 앞에 공백 없이 줄의 맨 앞에 있어야 합니다. “Script Dependencies”(스크립트 의존성)라는 텍스트는 대소문자와 관계없이 인식되며, 공백은 임의의 공백을 나타냅니다(단, 공백이 최소 하나는 있어야 합니다). 다음 정규 표현식은 의존성 블록 헤더 줄을 인식합니다.:

(?i)^#\s+script\s+dependencies:\s*$

의존성 블록을 읽는 도구는 표준 Python 인코딩 선언을 따를 수 있습니다(MAY). 따르지 않기로 한다면, 파일을 UTF-8로 처리해야 합니다(MUST).

헤더 줄 다음부터 # 기호로 시작하지 않는 첫 번째 줄 전까지, 파일의 모든 줄은 의존성 줄로 간주되며 다음과 같이 처리됩니다.

  1. 맨 앞의 # 기호를 제거합니다.
  2. 줄에 문자 시퀀스 “ # “(공백, 해시, 공백)가 포함되어 있으면 해당 문자와 그 뒤의 모든 문자를 버립니다. 이를 통해 의존성 블록에 인라인 주석을 넣을 수 있습니다.
  3. 남은 텍스트의 시작과 끝에 있는 공백을 버립니다.
  4. 이제 줄이 비어 있으면 그 줄은 무시합니다.
  5. 이제 줄의 내용은 반드시 유효한 PEP 508 의존성 지정자여야 합니다.

인라인 주석에서 # 앞뒤에 공백을 요구하는 것은 이를 PEP 508 URL 지정자의 일부(해시를 포함할 수 있지만 주변에 공백은 없음)와 구별하기 위해 필요합니다.

소비자는 최소한 모든 의존성이 PEP 508에서 정의한 name으로 시작하는지 반드시 검증해야 하며, 모든 의존성이 PEP 508을 완전히 준수하는지 검증할 수도 있습니다. 유효하지 않은 지정자를 발견하면 반드시 오류와 함께 실패해야 합니다.

예시

다음은 의존성 블록이 포함된 스크립트의 예입니다:

# In order to run, this script needs the following 3rd party libraries
#
# Script Dependencies:
#    requests
#    rich     # Needed for the output
#
#    # Not needed - just to show that fragments in URLs do not
#    # get treated as comments
#    pip @ https://github.com/pypa/pip/archive/1.3.1.zip#sha1=da9234ee9982d4bbb3c72346a6de940a148ea686

import requests
from rich.pretty import pprint

resp = requests.get("https://peps.python.org/api/peps.json")
data = resp.json()
pprint([(k, v["title"]) for k, v in data.items()][:10])

하위 호환성

의존성 블록은 구조화된 주석의 형태를 취하므로 기존 코드의 의미를 바꾸지 않고 추가할 수 있습니다.

의존성 블록의 형태와 일치하는 주석이 이미 존재할 가능성이 있습니다. 식별용 헤더 텍스트인 “Script Dependencies”(스크립트 의존성)는 이러한 위험을 최소화하도록 선택되었지만, 여전히 그런 일이 발생할 수 있습니다.

기존 주석이 의존성 블록으로 잘못 해석되는 드문 경우에는 코드의 더 앞부분에 실제 의존성 블록(스크립트에 의존성이 없다면 비어 있어도 됩니다)을 추가하여 이 문제를 해결할 수 있습니다.

보안 영향

의존성 블록이 포함된 스크립트를 의존성을 자동으로 설치하는 도구로 실행하면 임의의 코드가 다운로드되어 사용자 환경에 설치될 수 있습니다.

여기서의 위험은 스크립트를 실행하는 데 사용되는 도구의 기능에 속하므로 도구 자체에서 이미 다루고 있어야 합니다. 이 PEP가 새로 도입하는 유일한 추가 위험은 의존성 블록이 있는 신뢰할 수 없는 스크립트를 실행하여 잠재적으로 악의적인 의존성이 설치될 수 있는 경우입니다. 이 위험은 코드를 실행하기 전에 검토하는 일반적인 모범 관행으로 해결됩니다.

교육 방법

이 형식은 개발자가 설명용 주석에서 스크립트 의존성을 이미 명시하고 있을 법한 방식에 가깝도록 의도되었습니다. 요구되는 구조는 서식 규칙을 쉽게 익힐 수 있도록 의도적으로 최소화되어 있습니다.

사용자는 Python 의존성 지정자를 작성하는 방법을 알아야 합니다. 이는 PEP 508에서 다루지만, 간단한 예제(경험이 적은 사용자에게는 이것이 일반적일 것으로 예상됩니다)에서 문법은 패키지 이름만 쓰거나 이름과 버전 제한을 함께 쓰는 것이며, 이는 꽤 잘 알려진 문법입니다.

사용자는 의존성 데이터를 해석하는 도구를 사용하여 스크립트를 실행하는 방법도 알아야 합니다. 이는 이 PEP에서 다루지 않습니다. 그러한 도구를 어떻게 사용해야 하는지 문서화하는 것은 해당 도구의 책임이기 때문입니다.

핵심 Python 인터프리터는 의존성 블록을 해석하지 않는다는 점에 유의하십시오. 이는 python some_script.py를 실행하려다 왜 실패하는지 이해하지 못하는 초보자에게 혼란을 줄 수 있습니다. 하지만 이는 의존성이 설치되지 않은 상태에서 스크립트를 실행하면 오류가 발생하는 현재 상황과 다르지 않습니다.

일반적으로 초보자에게 의존성이 있는 스크립트가 주어진다면(의존성이 의존성 블록에 명시되어 있는지와 관계없이) 스크립트를 제공하는 사람이 그 스크립트를 실행하는 방법을 설명해야 하며, 스크립트 실행 도구를 사용해야 한다면 그 점도 언급해야 한다고 가정합니다.

권장 사항

이 절은 비규범적이며, 의존성 블록을 사용할 때의 “모범 사례”를 설명할 뿐입니다.

도구가 요구 사항을 최소한으로만 검증하는 것도 허용되지만, 실제로는 PEP 508 문법을 완전히 검사할 수 없더라도 가능한 한 많은 “기본 점검” 검증을 수행해야 합니다. 이렇게 하면 올바르게 종료되지 않은 의존성 블록을 일찍 보고할 수 있습니다. 요구 사항이 이름으로 시작하는지만 확인하는 최소한의 방식과 완전한 PEP 508 검증 사이의 좋은 절충안은, 이름만 있는지, 또는 이름 뒤에 선택적인 공백이 오고 그다음에 [ (extra), @ (urlspec), ; (marker) 중 하나나 (<!=>~ (version) 중 하나가 오는지 확인하는 것입니다.

스크립트는 일반적으로 의존성 블록을 파일 맨 위, 즉 셔뱅 줄이 있다면 그 바로 뒤나 스크립트 독스트링 바로 뒤에 두어야 합니다. 특히 의존성 블록은 항상 파일의 실행 가능한 코드보다 앞에 두어야 합니다. 이렇게 하면 사람이 읽을 때 의존성 블록을 쉽게 찾을 수 있습니다.

참조 구현

이 제안을 Python으로 구현하는 코드는 꽤 간단하므로 참조 구현을 여기에 포함할 수 있습니다.

import re
import tokenize
from packaging.requirements import Requirement

DEPENDENCY_BLOCK_MARKER = r"(?i)^#\s+script\s+dependencies:\s*$"

def read_dependency_block(filename):
    # Use the tokenize module to handle any encoding declaration.
    with tokenize.open(filename) as f:
        # Skip lines until we reach a dependency block (OR EOF).
        for line in f:
            if re.match(DEPENDENCY_BLOCK_MARKER, line):
                break
        # Read dependency lines until we hit a line that doesn't
        # start with #, or we are at EOF.
        for line in f:
            if not line.startswith("#"):
                break
            # Remove comments. An inline comment is introduced by
            # a hash, which must be preceded and followed by a
            # space.
            line = line[1:].split(" # ", maxsplit=1)[0]
            line = line.strip()
            # Ignore empty lines
            if not line:
                continue
            # Try to convert to a requirement. This will raise
            # an error if the line is not a PEP 508 requirement
            yield Requirement(line)

여기에서 제안하는 것과 비슷한 형식은 이미 pipx와 pip-run에서 지원됩니다.

기각된 아이디어

다른 메타데이터를 포함하지 않는 이유는 무엇입니까?

이 제안이 다루는 핵심 사용 사례는 독립 실행형 스크립트가 성공적으로 실행되는 데 필요한 의존성을 식별하는 것입니다. 이는 현실에서 흔히 겪는 문제이며, 현재는 스크립트 실행 도구가 구현마다 다른 방식으로 데이터를 저장하여 해결하고 있습니다. 저장 형식을 표준화하면 스크립트가 특정 실행 도구에 묶이지 않으므로 상호 운용성이 향상됩니다.

다른 형태의 메타데이터도 독립 실행형 스크립트에서 유용할 수 있다고 주장할 수는 있지만, 현시점에서 그 필요성은 대체로 이론적인 수준입니다. 실제로 스크립트는 다른 메타데이터를 사용하지 않거나, 이미 존재하고 널리 쓰이는(따라서 사실상 표준인) 형식으로 저장합니다. 예를 들어 README 형식의 텍스트가 필요한 스크립트는 보통 표준 Python 모듈 독스트링을 사용하고, 버전을 선언하려는 스크립트는 __version__ 변수를 두는 일반적인 관례를 따릅니다.

이 PEP에 대한 논의 중에 제기된 사례 중 하나는, 패키지의 Requires-Python 코어 메타데이터 항목에 빗대어 스크립트 실행에 필요한 최소 Python 버전을 선언하는 기능이었습니다. 패키지와 달리 스크립트는 보통 한 명의 사용자나 하나의 환경에서만 실행되며, 여러 버전의 Python을 함께 쓰는 경우가 드문 상황에서 실행됩니다. 따라서 스크립트에서는 이 메타데이터의 필요성이 훨씬 덜 중요합니다. 이를 뒷받침하는 추가 근거로, 현재 사용할 수 있는 두 가지 주요 스크립트 실행 도구인 pipx와 pip-run은 이 데이터를 스크립트에 포함하는 수단을 제공하지 않습니다.

표준 “메타데이터 컨테이너” 형식을 만들면 다양한 접근 방식을 통합할 수 있겠지만, 실제로는 통합해야 할 필요가 크지 않으며, 그에 따른 혼란 때문에 도입이 늦어지거나, 더 가능성이 높게는 스크립트 작성자들이 표준을 그냥 무시하게 될 것입니다.

따라서 이 제안은 무언가가 분명히 필요하지만 기존 표준이나 일반적인 관행이 없는 단 하나의 사용 사례에만 집중하기로 합니다.

줄마다 마커를 사용하지 않는 이유는 무엇입니까?

헤더가 있는 주석 블록을 사용하는 대신, 다음과 같이 각 줄에 마커를 사용하는 방법도 생각할 수 있습니다.:

# Script-Dependency: requests
# Script-Dependency: click

이 방식을 쓰면 줄을 하나씩 파싱하기는 더 쉬워지지만, 여러 가지 문제가 있습니다. 첫째로, 이 방식은 다소 장황하고 가독성이 떨어집니다. 이는 어떤 키워드를 선택하느냐에 따라 분명히 달라지지만, 제안된 모든 선택지가 (작성자가 보기에) 블록 주석 형식보다 가독성이 떨어졌습니다.

더 중요한 점은, 이 형식은 설계상 의존성 지정자가 모두 하나의 블록에 함께 있어야 한다고 요구할 수 없게 만든다는 것입니다. 그 결과, 사람이 파일 전체를 꼼꼼히 확인하지 않고서는 모든 의존성을 찾아냈는지 확신할 수 없습니다. 이 문제에 대한 자세한 논의는 아래의 “여러 의존성 블록을 허용하고 병합하지 않는 이유는 무엇입니까?” 질문을 참조하십시오.

마지막으로, 참조 구현에서 볼 수 있듯이 “주석 블록” 형식을 파싱하는 것은 실제로 이 형식을 파싱하는 것보다 크게 어렵지 않습니다.

의존성 블록에 별도 형식의 주석을 사용하지 않는 이유는 무엇입니까?

이 제안의 이전 버전에서는 의존성 블록을 식별하기 위해 ##를 사용했습니다. 그러나 안타깝게도 flake8 린터에는 주석의 첫 # 기호 뒤에 공백이 있어야 한다는 규칙이 구현되어 있습니다. PEP 작성자는 그 규칙이 잘못되었다고 생각하지만, 이 규칙은 기본적으로 활성화되어 있으므로 의존성 블록을 만나면 검사가 실패하게 됩니다.

또한 black 포매터는 ## 형식을 허용하기는 하지만, 그 외 대부분의 주석 형식에서는 # 뒤에 공백을 추가합니다. 즉, #% 같은 대안을 선택하면 자동 재포맷팅으로 인해 의존성 블록이 손상됩니다. # #처럼 공백을 포함하는 형식도 가능하지만, 일반 사용자에게는 덜 자연스럽습니다(공백을 빠뜨리는 것은 누구나 저지르기 쉬운 실수입니다).

린터와 포매터가 새 표준을 인식하도록 변경될 가능성도 있지만, 전용 접두사를 두는 이점이 전환 비용이나 사용자가 오래된 도구를 사용하고 있을 위험을 정당화할 만큼 충분하지 않아 보였습니다.

여러 의존성 블록을 허용하고 이를 병합하지 않는 이유는 무엇입니까?

사람이 읽을 때 두 번째 의존성 블록이 있다는 사실을 놓치기가 너무 쉽기 때문입니다. 이로 인해 단순히 스크립트 실행기가 예상치 못하게 추가 패키지를 내려받게 될 수도 있고, 심지어 (스크립트 본문에 두 번째 의존성 블록을 “숨겨서”) 악성 패키지를 사용자의 컴퓨터에 몰래 들여오는 수단이 될 수도 있습니다.

“신뢰할 수 없는 코드는 실행하지 말라”는 원칙이 여기에도 적용되기는 하지만, 그 이점이 위험을 감수할 만큼 충분하지 않습니다.

더 표준적인 데이터 형식(예: TOML)을 사용하지 않는 이유는 무엇입니까?

우선, 대안이 될 형식으로 현실적으로 선택할 수 있는 것은 TOML뿐입니다. Python 패키징은 구조화된 데이터에 대해 TOML로 표준화되어 있으며, YAML이나 JSON 같은 다른 형식을 사용하면 실질적인 이점 없이 복잡성과 혼란만 더해질 것입니다.

따라서 질문은 본질적으로 “왜 TOML을 사용하지 않습니까?”입니다.

“의존성 블록” 형식의 핵심 아이디어는 스크립트 안에서 주석으로 자연스럽게 읽히는 무언가를 정의하는 것입니다. 의존성 데이터는 도구에도, 사람이 읽을 때에도 유용하므로 사람이 읽을 수 있는 형식을 갖추는 것이 유익합니다. 반면에 TOML은 필연적으로 자체 문법을 가지며, 이는 기반이 되는 데이터로부터 주의를 분산시킵니다.

Python으로 스크립트를 작성하는 개발자는 Python이나 Python 패키징에 익숙하지 않은 경우가 많다는 점을 기억하는 것이 중요합니다. 이들은 대개 시스템 관리자나 데이터 분석가로, Python을 단순히 “더 나은 배치 파일”로 사용하고 있을 수 있습니다. 이러한 사용자에게 TOML 형식은 낯설 가능성이 매우 높으며, 그 문법은 이해하기 어렵고 그다지 직관적이지도 않을 것입니다. 이러한 개발자는 Stack Overflow(스택 오버플로) 같은 출처에서 의존성 지정자를 제대로 이해하지 못한 채 복사해 오는 경우가 많을 것입니다. 그러한 요구 사항을 TOML 구조 안에 넣어야 한다는 것은 추가적인 복잡성입니다 – 그리고 여기서의 목표는 이러한 사용자가 서드파티 라이브러리를 쉽게 사용할 수 있도록 하는 것임을 기억하는 것이 중요합니다.

게다가 TOML은 본질적으로 매우 일반적인 자료 구조를 지원하기 위한 유연한 형식입니다. TOML에서는 단순한 문자열 리스트를 작성하는 방법이 많으며, 경험이 부족한 사용자는 어떤 형식을 사용해야 할지 알기 어려울 것입니다.

또 다른 잠재적 문제는 범용 TOML 파서를 사용하면 어떤 경우에는 측정 가능한 수준의 성능 오버헤드가 발생할 수 있다는 점입니다. 작은 스크립트를 실행할 때 시작 시간이 문제로 자주 언급되므로, 이는 높은 성능을 목표로 하는 스크립트 실행기에 문제가 될 수 있습니다.

그리고 마지막으로, 스크립트에 의존성 데이터를 기록하려는 도구도 있을 것입니다 – 예를 들어 라이브러리 함수를 참조하면 import 문과 의존성 지정자를 자동으로 추가해 주는 기능을 갖춘 IDE가 그렇습니다. TOML 데이터를 편집할 수 있게 해 주는 라이브러리가 있기는 하지만, 사용자의 레이아웃을 항상 잘 보존하지는 못합니다. 이를 효과적으로 처리하는 라이브러리가 있다 하더라도, 모든 도구가 그러한 라이브러리를 사용하기를 기대하는 것은 이 PEP를 지원하는 코드에 상당한 부담을 지우는 일입니다.

인용 규칙이 없는 단순한 줄 단위 형식을 택했기 때문에, 의존성 데이터는 (사람과 도구 모두에게) 읽기 쉽고 작성하기도 쉽습니다. 이 형식은 TOML 같은 형식만큼 유연하지는 않지만, 이 사용 사례에는 애초에 그런 유연성이 필요하지 않습니다.

(제한될 수도 있는) Python 문법을 사용하지 않는 이유는 무엇입니까?

이 방식은 일반적으로 의존성을 관례적인 이름을 가진 (런타임) 리스트 변수에 저장하는 형태가 됩니다. 예를 들면 다음과 같습니다:

__requires__ = [
    "requests",
    "click",
]

그 밖에 정적인 여러 줄 문자열을 사용하거나 스크립트의 독스트링에 의존성을 포함하자는 제안도 있습니다.

이 제안의 가장 큰 문제는 의존성 데이터를 사용하는 모든 소비자가 Python 파서를 구현해야 한다는 점입니다. 문법을 제한하더라도 스크립트의 나머지 부분은 완전한 Python 문법을 사용할 것이며, 주변 코드와 분리하여 성공적으로 파싱할 수 있는 문법을 정의하려는 시도는 매우 어렵고 오류가 발생하기 쉬울 것입니다.

게다가 Python 문법은 릴리스마다 바뀝니다. 의존성 데이터를 추출하는 데 Python 파서가 필요하다면, 파서는 스크립트가 어떤 Python 버전을 대상으로 작성되었는지 알아야 하며, 범용 도구가 여러 Python 버전을 처리할 수 있는 파서를 갖추는 데 드는 부담은 감당할 수 없습니다.

위의 문제들을 해결할 수 있다 하더라도, 이 형식은 데이터를 런타임에 변경할 수 있다는 인상을 줄 것입니다. 그러나 일반적으로는 그렇지 않으며, 그렇게 하려는 코드는 예상치 못한 혼란스러운 동작을 겪게 됩니다.

마지막으로, 런타임에 의존성 데이터를 사용할 수 있는 것이 실질적으로 쓸모 있다는 근거가 없습니다. 그런 용도가 발견된다면, 소스를 파싱하여 데이터를 얻는 것으로 충분히 간단하게 해결할 수 있습니다 - read_dependency_block(__file__).

다만 pip-run 유틸리티가 이 접근 방식(의 확장된 형태)을 실제로 구현하고 있다는 점은 주목할 만합니다. pip-run 설계에 대한 추가 논의는 해당 프로젝트의 이슈 트래커에서 볼 수 있습니다.

스크립트에 pyproject.toml 파일을 내장하지 않는 이유는 무엇입니까?

우선 pyproject.toml은 TOML 기반 형식이므로, 형식으로서의 TOML에 관해 앞서 언급한 우려가 모두 적용됩니다. 그러나 pyproject.toml은 Python 패키징에서 사용하는 표준이며, 기존 표준을 재사용하자는 것은 그 자체의 장단점에 따라 검토할 가치가 있는 합리적인 제안입니다.

첫 번째 문제는 이 제안이 스크립트에 대해 pyproject.toml의 모든 내용을 지원하자는 뜻인 경우가 거의 없다는 점입니다. 스크립트는 휠과 같은 어떤 종류의 배포 가능한 산출물로도 “빌드”되도록 의도된 것이 아니므로(이 점에 대해서는 아래에서 더 자세히 다룹니다), 예를 들어 pyproject.toml의 [build-system] 섹션은 거의 의미가 없습니다. 그리고 pyproject.toml의 도구별 섹션이 스크립트에 유용할 수는 있겠지만, ruff 같은 도구가 이런 방식으로 파일별 설정을 지원하려 할지는 전혀 분명하지 않으며, 그 결과 사용자는 동작하리라고 기대했는데 동작하지 않아 혼란을 겪게 됩니다. 게다가 이런 종류의 도구별 설정은 더 큰 프로젝트 안의 개별 파일에도 똑같이 유용하므로, 자체 pyproject.toml을 가진 더 큰 프로젝트 안의 단일 파일에 pyproject.toml을 내장한다는 것이 무엇을 의미하는지 고려해야 합니다.

또한 pyproject.toml은 현재 휠로 빌드될 프로젝트에 초점이 맞춰져 있습니다. 휠로 빌드되도록 의도되지 않은 프로젝트에서 pyproject.toml을 어떻게 사용할지에 대한 논의가 진행 중이며, 그 문제가 해결되기 전까지는(해결에는 아마도 별도의 PEP가 몇 개 필요할 것입니다) 그런 방식으로 빌드되고 배포되도록 의도된 것이 분명히 아닌 스크립트에 pyproject.toml을 내장하는 것을 논의하기는 시기상조로 보입니다.

따라서 결론은(일부 경우에는 명시적으로 언급되었지만 모든 경우에 그런 것은 아닙니다) 이 제안이 pyproject.toml의 일부를 내장하자는 의미라는 것입니다. 일반적으로 이는 PEP 621의 [project] 섹션이거나, 심지어 그 섹션의 dependencies 항목뿐입니다.

이 시점에서 첫 번째 문제는 제안을 “pyproject.toml 내장”으로 규정하면 앞 단락에서 논의한 종류의 혼란을 부추기게 된다는 점입니다 - 개발자는 pyproject.toml의 모든 기능을 기대할 것이고, 차이점과 제약이 있을 때 혼란을 겪을 것입니다. 그러므로 이 제안은 단순히 내장된 TOML 형식을 사용하되, 구체적으로 pyproject.toml의 특정 부분의 구조를 재사용하자는 제안으로 보는 편이 더 낫습니다. 그러면 문제는 pyproject.toml에 익숙한 사람들에게 혼란을 주지 않으면서 그 구조를 어떻게 설명하느냐가 됩니다. pyproject.toml을 참조하여 설명하면 연관성은 여전히 남습니다. 그러나 독립적으로 설명하면 사람들은 그 구조의 “비슷하지만 다른” 성격 때문에 혼란을 겪을 것입니다.

또한 이 제안의 핵심 대상 독자 중 상당수가 Python을 그저 “더 나은 배치 파일” 용도로 사용하는 개발자라는 점을 기억하는 것도 중요합니다. 이러한 개발자는 대개 Python 패키징과 그 관례에 익숙하지 않으며, 패키징 도구의 “복잡성”과 “어려움”을 가장 강하게 비판하는 사람들인 경우가 많습니다. 그 결과, 기존 도구에 기반한 제안은 이 독자층에게 환영받지 못할 가능성이 높으며, 사람들이 기존의 임시방편을 그대로 계속 사용하면서 자신들의 삶을 더 편하게 만들려고 마련된 표준을 무시하는 결과로 쉽게 이어질 수 있습니다.

왜 import 문으로부터 요구 사항을 추론하지 않습니까?

이 아이디어는 소스 파일의 import 문을 자동으로 인식하여 요구 사항 목록으로 변환하는 것입니다.

그러나 이는 여러 가지 이유로 실현 불가능합니다. 첫째, 모든 Python 버전에서, 그리고 다른 언어로 작성된 도구에서도 문법을 쉽게 파싱할 수 있도록 유지해야 한다는 앞서 언급한 사항이 여기에도 똑같이 적용됩니다.

둘째, PyPI 및 Simple Repository API를 따르는 다른 패키지 저장소는 import되는 모듈 이름으로부터 패키지 이름을 알아내는 메커니즘을 제공하지 않습니다(이와 관련된 논의도 참고하십시오).

셋째, 저장소가 이 정보를 제공한다고 하더라도, 같은 import 이름이 PyPI의 여러 패키지에 대응할 수 있습니다. 원하는 패키지가 무엇인지 명확히 구분하는 작업은 같은 임포트 이름을 제공하는 프로젝트가 여러 개 있을 때만 필요하다고 반론할 수도 있습니다. 그러나 이렇게 하면 누구든 기존 프로젝트와 같은 import 이름을 제공하는 패키지를 PyPI에 올리는 것만으로 의도치 않게 또는 악의적으로 잘 동작하던 스크립트를 쉽게 망가뜨릴 수 있게 됩니다. 후보 중에서 색인에 가장 먼저 등록된 패키지를 선택하는 대안은, 인기 있는 패키지가 기존의 잘 알려지지 않은 패키지와 같은 import 이름으로 개발되는 경우 혼란을 일으킬 것이며, 기존 패키지가 재사용될 가능성이 높은 충분히 일반적인 import 이름으로 의도적으로 업로드된 악성 코드라면 해롭기까지 할 것입니다.

이와 관련된 아이디어로, 요구 사항을 블록에 모으는 대신 다음과 같은 문법으로 import 문에 주석으로 붙이는 방법이 있습니다.:

import numpy as np # requires: numpy
import rich # requires: rich

이 방법 역시 파싱의 어려움을 안고 있습니다. 또한 여러 줄에 걸친 import의 경우 주석을 어디에 두어야 할지 모호하며 보기 흉할 수 있습니다.:

from PyQt5.QtWidgets import (
    QCheckBox, QComboBox, QDialog, QDialogButtonBox,
    QGridLayout, QLabel, QSpinBox, QTextEdit
) # requires: PyQt5

게다가 이 문법은 모든 상황에서 직관적으로 예상하는 대로 동작할 수 없습니다. 다음을 생각해 보십시오.:

import platform
if platform.system() == "Windows":
    import pywin32 # requires: pywin32

여기서 사용자의 의도는 이 패키지가 Windows에서만 필요하다는 것이지만, 스크립트 실행기는 이를 이해할 수 없습니다(올바르게 작성하려면 requires: pywin32 ; sys_platform == 'win32'와 같이 써야 합니다).

(이 점에 대해 명확하게 논의해 주신 Jean Abou-Samra께 감사드립니다)

왜 그냥 런타임에 환경을 관리하지 않습니까?

의존성이 있는 스크립트를 실행하는 또 다른 접근 방식은 단순히 런타임에 해당 의존성을 관리하는 것입니다. 이는 패키지를 사용할 수 있게 해 주는 라이브러리를 사용하여 할 수 있습니다. 이러한 라이브러리를 구현하는 방법은 여러 가지가 있으며, 예를 들어 패키지를 사용자 환경에 직접 설치하거나, sys.path를 조작하여 로컬 캐시에서 사용할 수 있게 하는 방법이 있습니다.

이러한 접근 방식은 이 PEP와 양립할 수 없는 것이 아닙니다. 다음과 같은 API는

env_mgr.install("rich")
env_mgr.install("click")

import rich
import click

...

분명히 실현 가능합니다. 그러나 이러한 라이브러리는 새로운 표준 없이도 작성할 수 있으며, PEP 작성자가 아는 한 그런 일은 일어나지 않았습니다. 이는 이러한 접근 방식이 처음 보기만큼 매력적이지 않다는 것을 시사합니다. 또한 애초에 env_mgr 라이브러리를 사용할 수 있게 만들어야 하는 부트스트래핑 문제도 있습니다. 마지막으로, 이 접근 방식은 의존성 목록에 표준 형식을 사용하지 않으므로 다른 도구가 그 데이터에 접근할 수 없어, 실제로는 상호 운용성 측면의 이점을 전혀 제공하지 않습니다.

어떤 경우든 이러한 라이브러리도 스크립트 의존성 블록에서 설치할 패키지를 읽어 들이는 API를 포함할 수 있으므로, 여전히 이 제안의 혜택을 받을 수 있습니다. 이렇게 하면 동일한 기능을 제공하면서도 이 명세를 지원하는 다른 도구와의 상호 운용성을 확보할 수 있습니다.

# Script Dependencies:
#     rich
#     click
env_mgr.install_dependencies(__file__)

import rich
import click

...

그냥 pyproject.toml을 갖춘 Python 프로젝트를 구성하면 되지 않습니까?

다시 말하지만, 여기서 핵심 문제는 이 제안의 대상이 배포를 의도하지 않은 스크립트를 작성하는 사람들이라는 점입니다. 때로는 스크립트가 “공유”되기도 하지만, 이는 “배포”보다 훨씬 비공식적입니다. 보통 실행 방법을 적은 안내와 함께 스크립트를 이메일로 보내거나, 누군가에게 gist 링크를 전달하는 식입니다.

이러한 사용자에게 Python 패키징의 복잡한 내용을 배우라고 기대하는 것은 복잡성을 크게 높이는 일이며, “Python은 스크립트용으로 쓰기에는 너무 어렵다”는 인상을 줄 것이 거의 확실합니다.

게다가 여기서 기대하는 바가 pyproject.toml이 어떻게든 스크립트를 제자리에서 실행하도록 설계되는 것이라면, 이는 현재 존재하지 않는 표준의 새 기능입니다. 최소한 wheel로 배포되지 않을 프로젝트에 pyproject.toml을 사용하는 것에 관한 Discourse에서 진행 중인 논의가 결론이 나기 전까지는 이것이 합리적인 제안이 아닙니다. 그리고 그렇게 되더라도 “gist나 이메일로 누군가에게 스크립트를 보내는” 사용 사례는 해결되지 않습니다.

의존성에 requirements 파일을 사용하면 되지 않습니까?

요구 사항을 requirements 파일에 넣는 데에는 PEP가 필요하지 않습니다. 지금 당장 그렇게 할 수 있으며, 실제로 많은 임시방편 솔루션이 이렇게 하고 있을 가능성이 높습니다. 그러나 표준이 없으면 스크립트의 의존성 데이터를 어디에서 찾아야 하는지 알 방법이 없습니다. 더구나 requirements 파일 형식은 pip 전용이므로, 이에 의존하는 도구는 pip의 구현 세부 사항에 의존하게 됩니다.

따라서 표준을 만들려면 두 가지가 필요합니다.

  1. requirements 파일 형식을 대체할 표준화된 형식.
  2. 주어진 스크립트의 requirements 파일을 찾는 방법에 대한 표준.

첫 번째 항목은 상당히 큰 작업입니다. 여러 차례 논의되었지만, 지금까지 실제로 이를 시도한 사람은 아무도 없습니다. 가장 가능성 있는 접근 방식은 현재 requirements 파일로 처리하고 있는 개별 사용 사례별로 표준을 개발하는 것입니다. 여기서 한 가지 방법은 이 PEP가 PEP 508 요구 사항을 한 줄에 하나씩 담은 텍스트 파일이라는 새 파일 형식을 간단히 정의하는 것입니다. 그러면 그 파일을 어떻게 찾을 것인가 하는 문제만 남습니다.

여기서 “당연한” 해결책은 파일 이름을 스크립트와 같게 하되 .reqs 확장자(또는 그와 비슷한 것)를 붙이는 식일 것입니다. 그러나 이 방식은 현재는 파일 하나만 있으면 되는 상황에서 여전히 두 개의 파일을 요구하므로, “더 나은 배치 파일” 모델(셸 스크립트와 배치 파일은 일반적으로 자체 완결적입니다)과 맞지 않습니다. 개발자가 두 파일을 함께 유지해야 한다는 점을 기억해야 하는데, 이것이 항상 가능한 것은 아닙니다. 예를 들어, 시스템 관리 정책에 따라 특정 디렉터리의 모든 파일이 실행 가능해야 할 수 있습니다(예를 들어, Linux 파일 시스템 표준은 /usr/bin에 대해 이를 요구합니다). 또한 스크립트를 공유하는 일부 방법(예를 들어 Github의 gist 같은 텍스트 파일 공유 서비스나 회사 인트라넷에 게시하는 것)에서는 스크립트의 위치로부터 연관된 requirements 파일의 위치를 알아낼 수 없을 수도 있습니다(pipx 같은 도구는 URL에서 스크립트를 바로 실행하는 기능을 지원하므로, “스크립트와 그 의존성을 담은 zip 파일을 내려받아 압축을 푼다”는 요구 사항은 적절하지 않을 수 있습니다).

하지만 본질적으로 여기서 문제는, 형식이 스크립트 파일 자체에 의존성 데이터를 저장하는 것을 지원해야 한다는 요구 사항이 명시적으로 제시되어 있다는 점입니다. 그렇게 하지 않는 해결책은 그 요구 사항을 그냥 무시하는 것입니다.

스크립트가 패키지 색인을 지정할 수 있어야 합니까?

의존성 메타데이터는 코드가 어떤 패키지에 의존하는지에 관한 것이지, 그 패키지가 어디에서 오는지에 관한 것이 아닙니다. 이 점에서 스크립트용 메타데이터와 (pyproject.toml에 정의된) 배포 패키지용 메타데이터 사이에는 차이가 없습니다. 두 경우 모두 의존성은 어떻게 얻는지를 지정하지 않은 “추상적인” 형태로 주어집니다.

물론 의존성 정보를 사용하는 일부 도구는 구체적인 의존성 아티팩트의 위치를 찾아야 할 수도 있습니다. 예를 들어 해당 의존성을 포함하는 환경을 생성하려는 경우가 그렇습니다. 하지만 도구가 이를 수행하는 방식은 대체로 해당 도구의 UI와 밀접하게 연관되며, 이 PEP는 도구의 UI를 규정하려 하지 않습니다.

이 점에 대한 더 많은 논의, 특히 pip-run 도구가 선택한 UI에 대한 논의는 앞서 언급한 pip-run 이슈에서 확인할 수 있습니다.

로컬 의존성은 어떻습니까?

로컬 의존성은 특별한 메타데이터나 도구 없이도, 의존성의 위치를 sys.path에 추가하는 것만으로 처리할 수 있습니다. 이 경우에는 이 PEP가 전혀 필요하지 않습니다. 반면 “로컬 의존성”이 로컬에 게시된 실제 배포 패키지라면, 평소처럼 PEP 508 요구 사항으로 지정할 수 있으며, 로컬 패키지 색인은 도구를 실행할 때 해당 도구의 UI를 사용하여 지정하면 됩니다.

미해결 문제

현재로서는 없습니다.

참고 문헌