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
- 초록
- 동기
- 근거
- 명세
- 하위 호환성
- 보안 관련 사항
- 가르치는 방법
- 권장 사항
- 참조 구현
- 거부된 아이디어
- 다른 메타데이터를 포함하지 않는 이유는 무엇입니까?
- 줄마다 마커를 사용하지 않는 이유는 무엇입니까?
- 의존성 블록에 별도의 주석 형식을 사용하지 않는 이유는 무엇입니까?
- 여러 의존성 블록을 허용하고 병합하지 않는 이유는 무엇입니까?
- 더 표준적인 데이터 형식(예: TOML)을 사용하지 않는 이유는 무엇입니까?
- (제한된 형태일 수 있는) Python 구문을 사용하지 않는 이유는 무엇입니까?
- 스크립트에
pyproject.toml파일을 삽입하지 않는 이유는 무엇입니까? - import 문에서 요구 사항을 추론하면 안 됩니까?
- 런타임에 환경을 간단히 관리하면 안 됩니까?
pyproject.toml로 Python 프로젝트를 그냥 설정하면 안 됩니까?- 의존성을 위해 requirements 파일을 사용하면 안 됩니까?
- 스크립트에서 패키지 색인을 지정할 수 있어야 합니까?
- 로컬 의존성은 어떻습니까?
- 미해결 문제
- 참고 자료
- Copyright
번역·라이선스 안내
이 비공식 한국어 번역은 원문 Copyright 절의 Public Domain or CC0-1.0, whichever is more permissive 조건에 따라 제공합니다. 원저자와 공식 원문은 그대로 표시합니다. 수정되지 않은 기준 원문 · 공식 최신판
초록
이 PEP는 단일 파일 Python 스크립트에 서드파티 의존성을 포함하기 위한 형식을 명시합니다.
동기
모든 Python 코드가 자체 디렉터리를 갖고 pyproject.toml 파일을 포함하며 설치 가능한 배포 패키지로 빌드되는 의미에서 “프로젝트”로 구성되는 것은 아닙니다. Python은 셸 스크립트, 배치 파일 등의 (더 나은) 대안으로 Python 스크립트를 사용하는 스크립팅 언어로도 일상적으로 사용됩니다. 스크립트를 만들 때 Python 코드는 일반적으로 단일 파일로 저장되며, 흔히 이러한 “유틸리티 스크립트” 전용 디렉터리에 저장됩니다. 이 디렉터리는 여러 언어가 섞여 있고 Python은 그중 하나일 수 있습니다. 이러한 스크립트는 이메일처럼 간단한 방법이나 Github gist와 같은 URL로 연결되는 링크를 통해 공유될 수 있습니다. 그러나 일반적인 워크플로의 일부로 “배포”되거나 “설치”되는 경우는 일반적으로 아닙니다.
이러한 방식으로 Python을 스크립팅 언어로 사용할 때의 한 가지 문제는 스크립트에 필요한 서드파티 의존성이 포함된 환경에서 스크립트를 실행하는 방법입니다. 현재 이 문제를 해결하는 표준 도구는 없으며, 이 PEP는 그러한 도구를 정의하려고 시도하지 않습니다. 그러나 이 문제를 실제로 해결하는 모든 도구는 스크립트에 필요한 제3자 의존성이 무엇인지 알아야 합니다. 이러한 데이터를 저장하기 위한 표준 형식을 정의하면 기존 도구와 앞으로 만들어질 도구 모두 사용자가 스크립트에 도구별 메타데이터를 포함하도록 요구하지 않고도 해당 정보를 얻을 수 있습니다.
근거
핵심 요구 사항이 단일 파일 스크립트를 작성하고 스크립트 사본을 제공하여 간단히 공유하는 것이므로, 이 PEP는 외부 파일이 아니라 스크립트 자체에 의존성 데이터를 포함하기 위한 메커니즘을 정의합니다.
스크립트가 의존하는 서드파티 패키지에 관한 정보를 포함하는 의존성 블록이라는 개념을 정의합니다.
의존성 블록을 식별하려면 스크립트를 텍스트 파일로 간단히 읽을 수 있습니다. 이는 의도적인 설계입니다. Python 구문은 시간이 지나면서 변경되므로 스크립트를 Python 코드로 구문 분석하려면 특정 버전의 Python 구문을 선택해야 합니다. 또한 적어도 일부 도구는 Python으로 작성되지 않을 가능성이 있으며, 이러한 도구가 Python 구문 분석기를 구현하도록 요구하는 것은 지나친 부담입니다.
그러나 Python 핵심을 변경할 필요가 없도록 이 형식은 Python 구문 분석기에서 주석으로 보이도록 설계되었습니다. 의존성 블록이 주석으로 해석되지 않는 코드를 작성할 수 있습니다(예를 들어 Python 여러 줄 문자열에 삽입하는 방식입니다). 그러나 고의로 병적인 예를 만들려는 것이 아니라면 이러한 사용은 권장되지 않으며 쉽게 피할 수 있습니다.
다른 언어에서 스크립트가 의존성을 지정하도록 허용하는 방식을 review검토한 결과, 이와 같은 “구조화된 주석”이 흔히 사용되는 접근 방식임을 알 수 있습니다.
명세
이 절의 내용은 “Embedding Metadata in Script Files”라는 제목의 문서로 Python Packaging 사용자 가이드의 PyPA Specifications 절에 게시됩니다.
모든 Python 스크립트에는 의존성 블록이 포함될 수 있습니다. 의존성 블록은 스크립트를 텍스트 파일로 읽어(즉, 파일을 Python 소스 코드로 구문 분석하지 않고) 다음 형식의 첫 번째 줄을 찾는 방식으로 식별합니다.:
# Script Dependencies:
해시 문자는 줄의 시작 부분에 있어야 하며 앞에 공백이 없어야 합니다. “Script Dependencies”라는 텍스트는 대소문자와 관계없이 인식되며, 공백은 임의의 공백을 나타냅니다(단, 하나 이상의 공백이 있어야 합니다). 다음 정규 표현식은 의존성 블록 헤더 줄을 인식합니다.:
(?i)^#\s+script\s+dependencies:\s*$
의존성 블록을 읽는 도구는 표준 Python 인코딩 선언을 존중할 수 있습니다. 그렇게 하지 않기로 선택한 경우 해당 파일을 반드시 UTF-8로 처리해야 합니다.
헤더 줄 다음에는 # 기호로 시작하지 않는 첫 번째 줄까지 파일의 모든 줄이 의존성 줄로 간주되며 다음과 같이 처리됩니다.
- 처음의
#기호를 제거합니다. - 해당 줄에 문자 시퀀스 “ # “(공백, 해시, 공백)가 포함되어 있으면, 해당 문자와 그 뒤의 모든 문자를 폐기합니다. 이를 통해 종속성 블록에 인라인 주석을 포함할 수 있습니다.
- 남은 텍스트의 시작과 끝에 있는 공백을 폐기합니다.
- 이제 줄이 비어 있으면 무시합니다.
- 이제 줄의 내용은 유효한 PEP 508 종속성 지정자여야 합니다.
인라인 주석에서 #앞뒤에 공백을 요구하는 것은 이를 PEP 508 URL 지정자의 일부와 구별하기 위해 필요합니다(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 검증 사이의 좋은 절충안은, 단순한 이름 또는 선택적 공백이 뒤따르는 이름을 확인한 다음 [ (추가 항목), @ (urlspec), ; (마커) 또는 (<!=>~ (버전) 중 하나가 오는지 확인하는 것입니다.
일반적으로 스크립트는 파일의 맨 위에 의존성 블록을 배치해야 하며, 셰뱅 라인 바로 뒤나 스크립트 독스트링 바로 뒤에 배치해야 합니다. 특히 의존성 블록은 파일의 실행 가능한 코드보다 항상 앞에 배치해야 합니다. 이렇게 하면 사람이 의존성 블록을 쉽게 찾을 수 있습니다.
참조 구현
이 제안을 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)
거부된 아이디어
다른 메타데이터를 포함하지 않는 이유는 무엇입니까?
이 제안이 다루는 핵심 사용 사례는 독립 실행형 스크립트가 성공적으로 실행되기 위해 필요한 의존성을 식별하는 것입니다. 이는 현재 스크립트 실행기 도구가 구현별 데이터 저장 방식을 사용하여 해결하는 일반적인 실제 문제입니다. 저장 형식을 표준화하면 스크립트를 특정 실행기에 종속시키지 않으므로 상호 운용성이 향상됩니다.
독립 실행형 스크립트에서 다른 형태의 메타데이터가 유용할 수 있다는 주장은 가능하지만, 현재로서는 그 필요성이 대부분 이론적인 수준입니다. 실제로 스크립트는 다른 메타데이터를 사용하지 않거나, 이미 존재하는 널리 사용되는(따라서 사실상의 표준인) 형식에 이를 저장합니다. 예를 들어 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 파서를 사용하면 경우에 따라 측정 가능한 성능 오버헤드가 발생할 수 있다는 점입니다. 작은 스크립트를 실행할 때 시작 시간이 문제로 자주 언급되므로, 이는 높은 성능을 목표로 하는 스크립트 실행기에 문제가 될 수 있습니다.
마지막으로, 스크립트에 의존성 데이터를 작성하는 도구도 필요할 것입니다 – 예를 들어 라이브러리 함수를 참조할 때 가져오기와 의존성 지정자를 자동으로 추가하는 기능을 갖춘 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을 사용하는 방법에 관한 an ongoing discussion이 진행 중이며, 그 문제가 해결될 때까지(해결하려면 자체 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를 준수하는 다른 패키지 저장소는 가져온 모듈 이름에서 패키지 이름을 확인할 수 있는 메커니즘을 제공하지 않습니다(this related discussion도 참조하십시오).
셋째, 설령 저장소가 이러한 정보를 제공하더라도 동일한 import 이름이 PyPI의 여러 패키지에 대응할 수 있습니다. 동일한 import 이름을 제공하는 프로젝트가 여러 개 있는 경우에만 원하는 패키지를 판별하면 된다고 반론할 수도 있습니다. 그러나 이렇게 하면 누구나 기존 프로젝트와 동일한 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이 어떤 방식으로든 스크립트를 그 자리에서 실행하기 위한 용도로 설계될 것이라고 기대한다면, 이는 현재 존재하지 않는 표준의 새로운 기능입니다. 최소한, 배포 패키지로 배포되지 않을 프로젝트에 pyproject.toml을 사용하는 것에 관한 Discourse의 현재 논의가 해결되기 전까지는 합리적인 제안이 아닙니다. 설령 그렇다 하더라도, 이는 “gist나 이메일로 누군가에게 스크립트를 보내는” 사용 사례를 해결하지 못합니다.
의존성을 위해 requirements 파일을 사용하면 안 됩니까?
requirements 파일에 요구 사항을 넣는 데 PEP가 필요하지는 않습니다. 지금 당장 그렇게 할 수 있으며, 실제로 많은 임시방편적인 해결책이 이렇게 할 가능성이 큽니다. 그러나 표준이 없으면 스크립트의 의존성 데이터를 어디에서 찾을지 알 방법이 없습니다. 게다가 requirements 파일 형식은 pip 전용이므로, 이를 사용하는 도구는 pip 구현의 세부 사항에 의존하게 됩니다.
따라서 표준을 만들려면 두 가지가 필요합니다.
- requirements 파일 형식을 대체할 표준 형식입니다.
- 주어진 스크립트의 requirements 파일을 찾는 방법에 대한 표준입니다.
첫 번째 항목은 상당한 작업입니다. 여러 차례 논의되었지만, 지금까지 실제로 이를 시도한 사람은 없습니다. 가장 가능성 높은 접근 방식은 현재 requirements 파일로 해결하고 있는 개별 사용 사례를 위한 표준을 개발하는 것입니다. 한 가지 방법은 이 PEP에서 PEP 508 요구 사항을 한 줄에 하나씩 포함하는 새로운 텍스트 파일 형식을 단순히 정의하는 것입니다. 그러면 그 파일을 어디에서 찾을지에 대한 문제만 남습니다.
여기서 “명백한” 해결책은 파일 이름을 스크립트와 동일하게 지정하되, .reqs 확장자나 이와 유사한 확장자를 사용하는 것입니다. 그러나 이렇게 하면 현재는 파일 하나만 필요했던 곳에 여전히 two개의 파일이 필요하며, 따라서 “더 나은 배치 파일” 모델과 일치하지 않습니다(셸 스크립트와 배치 파일은 일반적으로 자체 완결형입니다). 개발자가 두 파일을 함께 보관해야 한다는 점을 기억해야 하며, 이것이 항상 가능하지는 않을 수 있습니다. 예를 들어, 시스템 관리 정책에 따라 특정 디렉터리의 all파일이 실행 가능해야 할 수 있습니다(예를 들어 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를 사용하여 로컬 패키지 색인을 지정할 수 있습니다.
미해결 문제
현재로서는 없습니다.
참고 자료
Copyright
This document is placed in the public domain or under the CC0-1.0-Universal license, whichever is more permissive.