[Rejected] PEP 722 - Dependency specification for single-file scripts

원문 링크: PEP 722 - Dependency specification for single-file scripts

상태: Rejected 유형: Standards Track 작성일: 19-Jul-2023

PEP 722 – 단일 파일 스크립트의 의존성(Dependency) 명세 (Rejected)

작성자: Paul Moore PEP 위임자: Brett Cannon 상태: 거부됨 (Rejected) - PEP 723으로 대체됨 생성일: 2023년 7월 19일 해결일: 2023년 10월 21일


개요 (Abstract)

이 PEP는 단일 파일 Python 스크립트에 서드파티 의존성(third-party dependencies)을 포함하기 위한 형식을 명시합니다.

동기 (Motivation)

모든 Python 코드가 pyproject.toml 파일을 포함하는 자체 디렉토리를 가진 “프로젝트” 형태로 구성되는 것은 아닙니다. Python은 셸 스크립트(shell scripts)나 배치 파일(batch files)의 대안으로 스크립팅 언어로서도 일상적으로 사용됩니다. 이러한 스크립트는 일반적으로 단일 파일로 저장되며, 여러 언어가 혼합된 “유틸리티 스크립트” 전용 디렉토리에 보관될 수 있습니다. 이 스크립트들은 이메일이나 GitHub gist URL 링크처럼 간단한 방식으로 공유되기도 합니다. 그러나 일반적으로 정상적인 워크플로우의 일부로 “배포”되거나 “설치”되지는 않습니다.

이러한 방식으로 Python을 스크립팅 언어로 사용할 때 한 가지 문제는 스크립트가 필요로 하는 모든 서드파티 의존성을 포함하는 환경에서 스크립트를 실행하는 방법입니다. 현재 이 문제를 해결하는 표준 도구가 없으며, 이 PEP는 그러한 도구를 정의하려고 시도하지 않습니다. 그러나 이 문제를 해결하는 모든 도구는 스크립트가 어떤 서드파티 의존성을 필요로 하는지 알아야 합니다. 이러한 데이터를 저장하기 위한 표준 형식을 정의함으로써, 기존 도구뿐만 아니라 향후의 모든 도구도 사용자가 스크립트에 도구별 메타데이터를 포함할 필요 없이 해당 정보를 얻을 수 있게 됩니다.

근거 (Rationale)

단일 파일 스크립트를 작성하고 스크립트 사본을 제공하여 간단하게 공유하는 것이 주요 요구사항이기 때문에, 이 PEP는 의존성 데이터를 외부 파일이 아닌 스크립트 자체 내에 임베딩(embedding)하는 메커니즘을 정의합니다.

이 PEP는 스크립트가 의존하는 서드파티 패키지에 대한 정보를 포함하는 의존성 블록(dependency block) 개념을 정의합니다.

의존성 블록을 식별하기 위해 스크립트는 단순히 텍스트 파일로 읽을 수 있습니다. 이는 Python 문법이 시간이 지남에 따라 변하기 때문에, 스크립트를 Python 코드로 파싱(parsing)하려고 시도하면 특정 Python 버전의 문법을 선택해야 한다는 문제점을 피하기 위함입니다. 또한, 최소한 일부 도구는 Python으로 작성되지 않을 가능성이 높으며, 이들에게 Python 파서(parser)를 구현하도록 기대하는 것은 너무 큰 부담입니다.

그러나 Python 코어에 변경사항을 요구하지 않기 위해, 이 형식은 Python 파서에게는 주석(comments)으로 보이도록 설계되었습니다. 의존성 블록이 주석(comment)으로 해석되지 않는 코드(예: Python 여러 줄 문자열 내에 임베딩)를 작성하는 것도 가능하지만, 이러한 사용은 권장되지 않으며 의도적으로 병리학적인(pathological) 예시를 만들려고 하지 않는 한 쉽게 피할 수 있습니다.

다른 언어들이 스크립트의 의존성을 명시하는 방식을 검토한 결과, 이러한 “구조화된 주석(structured comment)” 방식이 흔히 사용되는 접근 방식임을 알 수 있습니다.

명세 (Specification)

이 섹션의 내용은 Python Packaging 사용자 가이드의 PyPA 명세 섹션에 “스크립트 파일에 메타데이터 임베딩(Embedding Metadata in Script Files)”이라는 제목의 문서로 게시될 예정이었습니다.

어떤 Python 스크립트라도 의존성 블록을 포함할 수 있습니다. 의존성 블록은 스크립트를 텍스트 파일로 읽어(즉, 파일을 Python 소스 코드로 파싱하지 않음) 다음 형식의 첫 번째 줄을 찾아 식별됩니다.

# Script Dependencies:

해시( # ) 문자는 선행 공백 없이 줄의 시작 부분에 있어야 합니다. “Script Dependencies” 텍스트는 대소문자 구분 없이 인식되며, 공백은 임의의 공백을 나타냅니다(최소 하나 이상의 공백이 있어야 함). 다음 정규 표현식(regular expression)이 의존성 블록 헤더 줄을 인식합니다.

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

의존성 블록을 읽는 도구는 표준 Python 인코딩 선언을 존중할 수 있습니다(MAY). 그렇게 하지 않는 경우, UTF-8로 파일을 처리해야 합니다(MUST).

헤더 줄 다음에 파일 내에서 # 문자로 시작하지 않는 첫 번째 줄까지의 모든 줄은 의존성 줄로 간주되며 다음과 같이 처리됩니다.

  1. 초기 # 기호가 제거됩니다.
  2. 줄에 “ # “(공백, 해시, 공백) 문자열이 포함된 경우, 해당 문자열과 그 뒤의 모든 문자는 버려집니다. 이는 의존성 블록이 인라인 주석(inline comments)을 포함할 수 있도록 합니다.
  3. 남은 텍스트의 시작과 끝에 있는 공백은 제거됩니다.
  4. 줄이 비어 있으면 무시됩니다.
  5. 이제 줄의 내용은 유효한 PEP 508 의존성 명세자여야 합니다(MUST).

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

이용자(Consumers)는 최소한 모든 의존성이 PEP 508에 정의된 이름으로 시작하는지 검증해야 하며(MUST), 모든 의존성이 PEP 508을 완전히 준수하는지 검증할 수 있습니다(MAY). 유효하지 않은 명세자(specifier)를 발견하면 오류와 함께 실패해야(MUST) 합니다.

예시 (Example)

다음은 임베딩된 의존성 블록이 있는 스크립트의 예시입니다.

# 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])

하위 호환성 (Backwards Compatibility)

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

기존 주석이 의존성 블록의 형식과 일치할 가능성이 있습니다. 식별 헤더 텍스트인 “Script Dependencies”는 이러한 위험을 최소화하도록 선택되었지만, 여전히 가능성은 있습니다.

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

보안 관련 사항 (Security Implications)

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

이러한 위험은 스크립트를 실행하는 데 사용되는 도구의 기능 일부이며, 따라서 도구 자체에서 이미 다루어져야 합니다. 이 PEP가 도입하는 유일한 추가 위험은 의존성 블록이 있는 신뢰할 수 없는 스크립트가 실행될 때 잠재적으로 악성 의존성(malicious dependency)이 설치될 수 있다는 것입니다. 이 위험은 코드를 실행하기 전에 검토하는 일반적인 좋은 관행으로 해결됩니다.

교육 방법 (How to Teach This)

이 형식은 개발자가 스크립트 의존성을 설명 주석에 이미 명시하는 방식과 유사하도록 의도되었습니다. 필요한 구조는 의도적으로 최소화되어 형식 규칙을 배우기 쉽습니다.

사용자는 Python 의존성 명세자(dependency specifiers)를 작성하는 방법을 알아야 합니다. 이는 PEP 508에서 다루어지지만, 간단한 예시(경험이 없는 사용자에게 일반적일 것으로 예상됨)의 경우 문법은 패키지 이름 또는 이름과 버전 제한일 뿐이며, 이는 상당히 잘 이해되는 문법입니다.

사용자는 의존성 데이터를 해석하는 도구를 사용하여 스크립트를 실행하는 방법도 알아야 합니다. 이 PEP는 도구 사용법을 문서화하는 것이 해당 도구의 책임이므로 이 부분을 다루지 않습니다.

핵심 Python 인터프리터는 의존성 블록을 해석하지 않는다는 점에 유의해야 합니다. 이는 python some_script.py를 실행하려다 실패하는 이유를 이해하지 못하는 초보자들에게 혼란을 줄 수 있습니다. 그러나 의존성 없이 스크립트를 실행하면 오류가 발생하는 현재 상황과 다르지 않습니다.

일반적으로, 초보자가 의존성을 가진 스크립트(의존성 블록에 명시되었는지 여부와 관계없이)를 받으면, 스크립트를 제공하는 사람이 해당 스크립트를 실행하는 방법을 설명해야 하며, 스크립트 러너(script runner) 도구 사용이 포함된다면 이를 명시해야 합니다.

권장 사항 (Recommendations)

이 섹션은 규범적(non-normative)이지 않으며 의존성 블록을 사용할 때의 “모범 사례”를 설명합니다.

도구는 최소한의 요구사항 유효성 검사를 수행할 수 있지만(permitted), 실제로는 완전한 PEP 508 문법 검사를 수행할 수 없더라도 가능한 한 많은 “건전성 검사(sanity check)” 유효성 검사를 수행해야 합니다(should). 이는 올바르게 종료되지 않은 의존성 블록이 조기에 보고되도록 돕습니다. 요구사항이 이름으로 시작하는지만 확인하는 최소한의 접근 방식과 완전한 PEP 508 유효성 검사 사이의 좋은 절충안은, bare name 또는 선택적 공백 뒤에 [ (extra), @ (urlspec), ; (marker) 또는 <!=>~ (version) 중 하나가 오는 이름을 확인하는 것입니다.

스크립트는 일반적으로 의존성 블록을 파일 상단에, 셔뱅 라인(shebang line) 바로 뒤 또는 스크립트 독스트링(docstring) 바로 뒤에 배치해야 합니다(should). 특히, 의존성 블록은 항상 파일 내의 실행 가능한 코드보다 먼저 배치되어야 합니다. 이렇게 하면 사람이 쉽게 찾을 수 있습니다.

참고 구현 (Reference Implementation)

이 제안을 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):
    # 인코딩 선언을 처리하기 위해 tokenize 모듈을 사용합니다.
    with tokenize.open(filename) as f:
        # 의존성 블록에 도달할 때까지 (또는 EOF까지) 줄을 건너뜁니다.
        for line in f:
            if re.match(DEPENDENCY_BLOCK_MARKER, line):
                break
        # '#'으로 시작하지 않는 줄을 만나거나 EOF에 도달할 때까지
        # 의존성 줄을 읽습니다.
        for line in f:
            if not line.startswith("#"):
                break
            # 주석을 제거합니다. 인라인 주석은
            # 해시로 시작하며, 앞뒤에 공백이 있어야 합니다.
            line = line[1:].split(" # ", maxsplit=1)[0]
            line = line.strip()
            # 빈 줄은 무시합니다.
            if not line:
                continue
            # 요구사항으로 변환을 시도합니다.
            # 줄이 PEP 508 요구사항이 아니면 오류가 발생합니다.
            yield Requirement(line)

여기서 제안된 형식과 유사한 형식은 이미 pipxpip-run에서 지원됩니다.

거부된 아이디어 (Rejected Ideas)

PEP 722 논의 과정에서 다양한 대안이 검토되었으나, 현재 제안된 방식의 이점을 고려하여 거부되었습니다. 주요 거부 사유는 다음과 같습니다.

  • 다른 메타데이터 포함하지 않는 이유: 스크립트 실행에 필요한 의존성 식별이라는 핵심 사용 사례에 집중하기 위함입니다. 다른 메타데이터(예: Python 최소 버전)의 필요성은 이론적이며, 기존 스크립트 러너(script runner) 도구에서도 지원되지 않습니다.
  • 줄마다 마커를 사용하지 않는 이유: # Script-Dependency:와 같이 각 줄에 마커를 사용하는 방식은 장황하고 가독성이 떨어지며, 모든 의존성이 하나의 블록에 모여 있어야 한다는 요구사항을 강제할 수 없습니다.
  • 의존성 블록에 다른 형태의 주석을 사용하지 않는 이유 (예: ##): flake8과 같은 린터(linter)와의 충돌(예: ## 뒤에 공백이 없으면 오류), black과 같은 포매터(formatter)가 자동으로 주석 형식을 변경하여 의존성 블록을 손상시킬 가능성 때문입니다.
  • 여러 의존성 블록을 허용하고 병합하지 않는 이유: 사람이 두 번째 의존성 블록의 존재를 놓치기 쉽고, 이는 예기치 않은 패키지 다운로드 또는 악성 패키지 삽입의 위험으로 이어질 수 있습니다.
  • 더 표준적인 데이터 형식(예: TOML)을 사용하지 않는 이유: TOML은 스크립트 작성에 익숙하지 않은 시스템 관리자나 데이터 분석가 같은 사용자에게 익숙하지 않고 문법이 복잡할 수 있습니다. 또한, 유연한 TOML 형식은 단순한 의존성 목록에 과도하며, 파싱 성능 오버헤드와 도구의 레이아웃 유지 문제도 고려되었습니다.
  • Python 문법(예: __requires__ 변수)을 사용하지 않는 이유: 이 방식은 의존성 데이터를 파싱하기 위해 Python 파서를 구현해야 하며, Python 문법 변화에 대한 관리 부담이 큽니다. 또한 런타임에 데이터가 변경될 수 있다는 오해를 줄 수 있습니다.
  • pyproject.toml 파일을 스크립트에 임베딩하지 않는 이유: pyproject.toml은 프로젝트 빌드 및 배포에 중점을 둔 형식으로, 단일 스크립트에는 맞지 않는 부분이 많습니다. 또한 스크립트 작성 대상이 Python 패키징에 익숙하지 않은 사용자임을 고려할 때, 기존 복잡한 솔루션을 재사용하는 것은 오히려 혼란을 가중시킬 수 있습니다.
  • import 문에서 요구사항을 추론하지 않는 이유: PyPI에는 모듈 이름으로 패키지 이름을 해결하는 메커니즘이 없으며, 동일한 import 이름에 여러 패키지가 대응될 수 있어 모호성과 보안 위험이 있습니다. import 문에 직접 주석으로 요구사항을 붙이는 방식도 파싱 난이도와 모호성 문제가 있습니다.
  • 런타임에 환경을 관리하지 않는 이유: env_mgr.install("rich")와 같은 런타임 설치 방식은 이 PEP와 상호 배타적이지 않지만, 표준 없이 구현하기 어렵고 상호 운용성 이점을 제공하지 않습니다.
  • pyproject.toml을 사용하여 Python 프로젝트를 설정하지 않는 이유: 이 PEP의 대상은 배포 목적이 아닌 스크립트를 작성하는 사용자입니다. 이러한 사용자에게 Python 패키징의 복잡성을 요구하는 것은 “Python이 스크립트에 너무 어렵다”는 인식을 줄 수 있습니다.
  • requirements.txt 파일을 사용하지 않는 이유: requirements.txtpip에 특화된 형식이며, 표준이 없으면 스크립트의 의존성 데이터를 찾는 방법을 알 수 없습니다. 또한 스크립트와 별도의 파일이 필요하여 단일 파일 스크립트라는 요구사항에 부합하지 않습니다.
  • 스크립트가 패키지 인덱스를 지정하지 않는 이유: 의존성 메타데이터는 패키지 자체에 관한 것이지, 패키지를 어디서 가져오는지에 관한 것이 아닙니다. 이는 배포 패키지 메타데이터와 동일하게 추상적인 형태로 유지되어야 합니다.
  • 로컬 의존성(local dependencies) 처리: 로컬 의존성은 sys.path에 경로를 추가하는 방식으로 특별한 메타데이터나 도구 없이 처리할 수 있으므로, 이 PEP의 범위 밖입니다.

최종 결론 (Resolution)

이 PEP는 PEP 723으로 대체되어 거부(Rejected)되었습니다. PEP 723은 이 PEP와 유사한 문제를 다루지만, 다른 방식으로 접근합니다.

여기서 제안된 형식과 유사한 형식은 이미 pipxpip-run에서 지원됩니다. [cite: 1]

거부된 아이디어 (Rejected Ideas)

PEP 722 논의 과정에서 다양한 대안이 검토되었으나, 현재 제안된 방식의 이점을 고려하여 거부되었습니다. 주요 거부 사유는 다음과 같습니다. [cite: 1]

  • 다른 메타데이터 포함하지 않는 이유: 스크립트 실행에 필요한 의존성 식별이라는 핵심 사용 사례에 집중하기 위함입니다. 다른 메타데이터(예: Python 최소 버전)의 필요성은 이론적이며, 기존 스크립트 러너(script runner) 도구에서도 지원되지 않습니다. [cite: 1]
  • 줄마다 마커를 사용하지 않는 이유: # Script-Dependency:와 같이 각 줄에 마커를 사용하는 방식은 장황하고 가독성이 떨어지며, 모든 의존성이 하나의 블록에 모여 있어야 한다는 요구사항을 강제할 수 없습니다. [cite: 1]
  • 의존성 블록에 다른 형태의 주석을 사용하지 않는 이유 (예: ##): flake8과 같은 린터(linter)와의 충돌(예: ## 뒤에 공백이 없으면 오류), black과 같은 포매터(formatter)가 자동으로 주석 형식을 변경하여 의존성 블록을 손상시킬 가능성 때문입니다. [cite: 1]
  • 여러 의존성 블록을 허용하고 병합하지 않는 이유: 사람이 두 번째 의존성 블록의 존재를 놓치기 쉽고, 이는 예기치 않은 패키지 다운로드 또는 악성 패키지 삽입의 위험으로 이어질 수 있습니다. [cite: 1]
  • 더 표준적인 데이터 형식(예: TOML)을 사용하지 않는 이유: TOML은 스크립트 작성에 익숙하지 않은 시스템 관리자나 데이터 분석가 같은 사용자에게 익숙하지 않고 문법이 복잡할 수 있습니다. 또한, 유연한 TOML 형식은 단순한 의존성 목록에 과도하며, 파싱 성능 오버헤드와 도구의 레이아웃 유지 문제도 고려되었습니다. [cite: 1]
  • Python 문법(예: __requires__ 변수)을 사용하지 않는 이유: 이 방식은 의존성 데이터를 파싱하기 위해 Python 파서를 구현해야 하며, Python 문법 변화에 대한 관리 부담이 큽니다. 또한 런타임에 데이터가 변경될 수 있다는 오해를 줄 수 있습니다. [cite: 1]
  • pyproject.toml 파일을 스크립트에 임베딩하지 않는 이유: pyproject.toml은 프로젝트 빌드 및 배포에 중점을 둔 형식으로, 단일 스크립트에는 맞지 않는 부분이 많습니다. 또한 스크립트 작성 대상이 Python 패키징에 익숙하지 않은 사용자임을 고려할 때, 기존 복잡한 솔루션을 재사용하는 것은 오히려 혼란을 가중시킬 수 있습니다. [cite: 1]
  • import 문에서 요구사항을 추론하지 않는 이유: PyPI에는 모듈 이름으로 패키지 이름을 해결하는 메커니즘이 없으며, 동일한 import 이름에 여러 패키지가 대응될 수 있어 모호성과 보안 위험이 있습니다. import 문에 직접 주석으로 요구사항을 붙이는 방식도 파싱 난이도와 모호성 문제가 있습니다. [cite: 1]
  • 런타임에 환경을 관리하지 않는 이유: env_mgr.install("rich")와 같은 런타임 설치 방식은 이 PEP와 상호 배타적이지 않지만, 표준 없이 구현하기 어렵고 상호 운용성 이점을 제공하지 않습니다. [cite: 1]
  • pyproject.toml을 사용하여 Python 프로젝트를 설정하지 않는 이유: 이 PEP의 대상은 배포 목적이 아닌 스크립트를 작성하는 사용자입니다. 이러한 사용자에게 Python 패키징의 복잡성을 요구하는 것은 “Python이 스크립트에 너무 어렵다”는 인식을 줄 수 있습니다. [cite: 1]
  • requirements.txt 파일을 사용하지 않는 이유: requirements.txtpip에 특화된 형식이며, 표준이 없으면 스크립트의 의존성 데이터를 찾는 방법을 알 수 없습니다. 또한 스크립트와 별도의 파일이 필요하여 단일 파일 스크립트라는 요구사항에 부합하지 않습니다. [cite: 1]
  • 스크립트가 패키지 인덱스를 지정하지 않는 이유: 의존성 메타데이터는 패키지 자체에 관한 것이지, 패키지를 어디서 가져오는지에 관한 것이 아닙니다. 이는 배포 패키지 메타데이터와 동일하게 추상적인 형태로 유지되어야 합니다. [cite: 1]
  • 로컬 의존성(local dependencies) 처리: 로컬 의존성은 sys.path에 경로를 추가하는 방식으로 특별한 메타데이터나 도구 없이 처리할 수 있으므로, 이 PEP의 범위 밖입니다. [cite: 1]

최종 결론 (Resolution)

이 PEP는 PEP 723으로 대체되어 거부(Rejected)되었습니다. [cite: 1] PEP 723은 이 PEP와 유사한 문제를 다루지만, 다른 방식으로 접근합니다.

⚠️ 알림: 이 문서는 AI를 활용하여 번역되었으며, 기술적 정확성을 보장하지 않습니다. 정확한 내용은 반드시 원문을 확인하시기 바랍니다.

Comments