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

Python 개선 제안 한국어 번역

PEP 723 – 인라인 스크립트 메타데이터

Author:
Ofek Lev <ofekmeister at gmail.com>
Sponsor:
Adam Turner <adam at python.org>
PEP-Delegate:
Brett Cannon <brett at python.org>
Discussions-To:
Discourse thread
Status:
Final
Type:
Standards Track
Topic:
Packaging
Created:
04-Aug-2023
Post-History:
04-Aug-2023, 06-Aug-2023, 23-Aug-2023, 06-Dec-2023
Replaces:
722
Resolution:
08-Jan-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, Inline script metadata, is maintained on the PyPA specs page.

×

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

초록

이 PEP는 실행기, IDE 및 이러한 스크립트와 상호 작용해야 할 수 있는 기타 외부 도구를 지원하기 위해 단일 파일 Python 스크립트에 삽입할 수 있는 메타데이터 형식을 지정합니다.

동기

Python은 셸 스크립트, 배치 파일 등을 대신하는 (더 나은) 대안으로서 Python 스크립트와 함께 스크립팅 언어로 일상적으로 사용됩니다. Python 코드가 스크립트로 구성될 때는 일반적으로 단일 파일로 저장되며, 가져오기에 사용될 수 있는 다른 로컬 코드의 존재를 전제로 하지 않습니다. 따라서 이메일, 스크립트 URL 또는 심지어 채팅 창과 같은 임의의 텍스트 기반 수단을 통해 다른 사람과 공유할 수 있습니다. 이와 같이 구성된 코드는 자체 디렉터리와 pyproject.toml 파일을 갖춘 완전한 프로젝트가 되지 않은 채 영원히 단일 파일로 남을 수도 있습니다.

이 접근 방식에서 사용자가 마주치는 문제는 이러한 스크립트를 실행하는 것이 작업인 도구의 메타데이터를 정의할 표준 메커니즘이 없다는 점입니다. 예를 들어 스크립트를 실행하는 도구는 어떤 의존성이 필요한지 또는 지원되는 Python 버전이 무엇인지 알아야 할 수 있습니다.

현재 이 문제를 해결하는 표준 도구는 없으며, 이 PEP는 그러한 도구를 정의하려고 시도하지 않습니다. 그러나 이 문제를 실제로 해결하는 모든 도구는 스크립트의 런타임 요구 사항이 무엇인지 알아야 합니다. 이러한 메타데이터를 저장하기 위한 표준 형식을 정의하면 기존 도구와 향후의 모든 도구가 사용자가 스크립트에 도구별 메타데이터를 포함하지 않아도 해당 정보를 얻을 수 있습니다.

근거

이 PEP는 외부 파일이 아니라 스크립트 자체에 메타데이터를 삽입하는 메커니즘을 정의합니다.

메타데이터 형식은 Python 프로젝트 디렉터리의 pyproject.toml 파일에 있는 데이터의 배치와 유사하게 설계되어, Python 프로젝트 작성 경험이 있는 사용자에게 익숙한 사용 경험을 제공합니다. 유사한 형식을 사용하면 패키징 도구 간의 불필요한 불일치를 피할 수 있으며, 이는 최근 패키징 설문조사에서 사용자들이 표현한 공통된 불만 사항입니다.

다음은 이 PEP가 지원하고자 하는 몇 가지 사용 사례입니다.

  • 스크립트를 실행할 수 있는 사용자용 CLI입니다. Hatch를 예로 들면 인터페이스는 단순히 hatch run /path/to/script.py [args]이며, Hatch가 해당 스크립트의 환경을 관리합니다. 이러한 도구는 Windows가 아닌 시스템에서 셰뱅 줄로 사용할 수 있습니다. 예를 들어 #!/usr/bin/env hatch run과 같이 사용할 수 있습니다.
  • 디렉터리형 프로젝트로 전환하려는 스크립트입니다. 사용자는 로컬 또는 원격 REPL 환경에서 신속하게 프로토타이핑한 다음, 아이디어가 잘 풀리면 더 정식적인 프로젝트 레이아웃으로 전환하기로 결정할 수 있습니다. 스크립트에서 의존성을 정의할 수 있으면 완전히 재현 가능한 버그 보고서를 작성하는 데 매우 유용할 것입니다.
  • 수동 의존성 관리를 피하려는 사용자입니다. 예를 들어 의존성을 추가/제거하는 명령을 제공하는 패키지 관리자나, 새 버전에 따라 또는 CVE에 대응하여 트리거되는 CI의 의존성 업데이트 자동화가 있습니다 [1].

사양

이 PEP는 reStructuredText 지시문에서 [2] 느슨하게 영감을 받은 메타데이터 주석 블록 형식을 정의합니다.

모든 Python 스크립트에는 최상위 주석 블록이 있을 수 있으며, 해당 블록은 콘텐츠 처리 방식을 결정하는 TYPE이 포함된 # /// TYPE 행으로 반드시 시작해야 합니다. 즉, 단일 #뒤에 단일 공백, 이어서 슬래시 세 개, 다시 단일 공백, 마지막으로 메타데이터 유형이 와야 합니다. 블록은 # ///줄로 끝나야 합니다. 즉, 단일 #뒤에 단일 공백, 이어서 슬래시 세 개가 와야 합니다. TYPE은 ASCII 문자, 숫자 및 하이픈으로만 구성되어야 합니다.

이 두 줄(# /// TYPE# ///) 사이의 모든 줄은 #로 시작하는 주석이어야 합니다. #뒤에 문자가 있는 경우 첫 번째 문자는 공백이어야 합니다. 임베디드 콘텐츠는 두 번째 문자가 공백인 경우 각 줄의 처음 두 문자를 제거하고, 그렇지 않으면 첫 번째 문자만 제거하여 구성합니다(즉, 해당 줄은 단일 #로만 구성됩니다).

다음 줄이 위에서 설명한 유효한 임베디드 콘텐츠 줄이 아닌 경우 종료 줄 # ///에 우선권이 부여됩니다. 예를 들어, 다음은 완전히 유효한 단일 블록입니다.

# /// some-toml
# embedded-csharp = """
# /// <summary>
# /// text
# ///
# /// </summary>
# public class MyClass { }
# """
# ///

시작 줄과 종료 줄 사이에 또 다른 시작 줄을 배치해서는 안 됩니다. 이러한 경우 도구는 오류를 생성할 수 있습니다. 닫히지 않은 블록은 무시해야 합니다.

동일한 TYPE으로 정의된 주석 블록이 여러 개 있는 경우 도구는 오류를 생성해야 합니다.

임베디드 메타데이터를 읽는 도구는 표준 Python 인코딩 선언을 따를 수 있습니다. 그렇게 하지 않기로 선택한 경우 파일을 UTF-8로 처리해야 합니다.

다음은 메타데이터를 구문 분석하는 데 사용할 수 있는 표준 정규 표현식입니다.

(?m)^# /// (?P<type>[a-zA-Z0-9-]+)$\s(?P<content>(^#(| .*)$\s)+)^# ///$

텍스트 사양과 정규 표현식 사이에 불일치가 있는 경우 텍스트 사양이 우선합니다.

도구는 이 PEP 또는 향후 PEP에서 표준화되지 않은 유형의 메타데이터 블록을 읽어서는 안 됩니다.

스크립트 유형

첫 번째 메타데이터 블록 유형의 이름은 script이며, 스크립트 메타데이터(종속성 데이터 및 도구 구성)를 포함합니다.

이 문서에는 최상위 필드 dependenciesrequires-python이 포함될 수 있으며, 선택적으로 [tool] 테이블을 포함할 수도 있습니다.

[tool]테이블은 도구, 스크립트 실행기 또는 그 밖의 프로그램에서 동작을 구성하는 데 사용할 수 있습니다. 이는 pyproject.toml에 있는 tool table와 동일한 의미를 가집니다.

최상위 필드는 다음과 같습니다.

  • dependencies: 스크립트의 런타임 종속성을 지정하는 문자열 목록입니다. 각 항목은 유효한 PEP 508 종속성이어야 합니다.
  • requires-python: 스크립트와 호환되는 Python 버전을 지정하는 문자열입니다. 이 필드의 값은 유효한 version specifier여야 합니다.

지정된 dependencies를 제공할 수 없는 경우 스크립트 실행기는 오류를 발생시켜야 합니다. 지정된 requires-python을 충족하는 Python 버전을 제공할 수 없는 경우 스크립트 실행기는 오류를 발생시키는 것이 좋습니다.

예시

다음은 메타데이터가 포함된 스크립트의 예입니다:

# /// script
# requires-python = ">=3.11"
# dependencies = [
#   "requests<3",
#   "rich",
# ]
# ///

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

참조 구현

다음은 Python 3.11 이상에서 메타데이터를 읽는 방법의 예입니다.

import re
import tomllib

REGEX = r'(?m)^# /// (?P<type>[a-zA-Z0-9-]+)$\s(?P<content>(^#(| .*)$\s)+)^# ///$'

def read(script: str) -> dict | None:
    name = 'script'
    matches = list(
        filter(lambda m: m.group('type') == name, re.finditer(REGEX, script))
    )
    if len(matches) > 1:
        raise ValueError(f'Multiple {name} blocks found')
    elif len(matches) == 1:
        content = ''.join(
            line[2:] if line.startswith('# ') else line[1:]
            for line in matches[0].group('content').splitlines(keepends=True)
        )
        return tomllib.loads(content)
    else:
        return None

도구는 패키지 관리자나 CI의 의존성 업데이트 자동화와 같이 의존성을 편집하는 경우가 많습니다. 다음은 tomlkit 라이브러리를 사용하여 내용을 수정하는 간단한 예입니다.

import re

import tomlkit

REGEX = r'(?m)^# /// (?P<type>[a-zA-Z0-9-]+)$\s(?P<content>(^#(| .*)$\s)+)^# ///$'

def add(script: str, dependency: str) -> str:
    match = re.search(REGEX, script)
    content = ''.join(
        line[2:] if line.startswith('# ') else line[1:]
        for line in match.group('content').splitlines(keepends=True)
    )

    config = tomlkit.parse(content)
    config['dependencies'].append(dependency)
    new_content = ''.join(
        f'# {line}' if line.strip() else f'#{line}'
        for line in tomlkit.dumps(config).splitlines(keepends=True)
    )

    start, end = match.span('content')
    return script[:start] + new_content + script[end:]

이 예에서는 TOML 형식을 보존하는 라이브러리를 사용했다는 점에 유의하십시오. 이는 편집을 위한 필수 요구 사항이 아니라, 있으면 좋은 기능입니다.

다음은 임의의 메타데이터 블록 스트림을 읽는 방법의 예입니다.

import re
from typing import Iterator

REGEX = r'(?m)^# /// (?P<type>[a-zA-Z0-9-]+)$\s(?P<content>(^#(| .*)$\s)+)^# ///$'

def stream(script: str) -> Iterator[tuple[str, str]]:
    for match in re.finditer(REGEX, script):
        yield match.group('type'), ''.join(
            line[2:] if line.startswith('# ') else line[1:]
            for line in match.group('content').splitlines(keepends=True)
        )

하위 호환성

작성 시점에는 블록 주석 시작 구문인 # /// scriptGitHub에서 어떤 Python 파일에도 나타나지 않습니다. 따라서 이 PEP로 인해 기존 스크립트가 손상될 위험은 거의 없습니다.

보안 영향

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

여기서 위험은 스크립트를 실행하는 데 사용되는 도구 기능의 일부이므로, 도구 자체에서 이미 처리하고 있어야 합니다. 이 PEP가 도입하는 유일한 추가 위험은 메타데이터가 포함된 신뢰할 수 없는 스크립트를 실행할 때 악의적인 의존성이나 전이 의존성이 설치될 수 있다는 점입니다.

이 위험은 코드를 실행하기 전에 검토하는 일반적인 모범 사례로 해결됩니다. 또한 도구에서 이 위험을 완화하기 위해 잠금 기능을 제공할 수도 있습니다.

이 내용을 가르치는 방법

스크립트에 메타데이터를 포함하려면 # /// script 줄로 시작하고 # /// 줄로 끝나는 주석 블록을 정의하십시오. 이 두 줄 사이의 모든 줄은 주석이어야 하며, 전체 내용은 처음 두 문자를 제거하여 도출됩니다.

# /// script
# dependencies = [
#   "requests<3",
#   "rich",
# ]
# requires-python = ">=3.11"
# ///

허용된 필드는 다음 표에 설명되어 있습니다:

필드 설명 도구 동작
dependencies 스크립트의 런타임 의존성을 지정하는 문자열의 리스트입니다. 각 항목은 유효한 PEP 508 의존성이어야 합니다. 지정된 의존성을 제공할 수 없는 경우 도구는 오류를 발생시킵니다.
requires-python 스크립트가 호환되는 Python 버전(들)을 지정하는 문자열입니다. 이 필드의 값은 유효한 버전 지정자여야 합니다. 제약 조건을 만족하는 Python 버전을 실행할 수 없는 경우 도구는 오류를 발생시킬 수 있습니다.

또한, [tool] 테이블이 허용됩니다. 허용되는 항목의 세부 사항은 pyproject.toml에서 허용되는 항목과 유사하지만, 관련 도구의 문서에 정확한 정보가 포함되어야 합니다.

포함된 메타데이터를 바탕으로 동작을 변경할지 여부는 개별 도구에 달려 있습니다. 예를 들어 모든 스크립트 실행기가 requires-python 필드에 정의된 특정 Python 버전을 위한 환경을 제공할 수 있는 것은 아닙니다.

어떤 도구, 스크립트 실행기 또는 그 밖의 도구에서도 tool table을 사용하여 동작을 구성할 수 있습니다.

권장 사항

서로 다른 Python 버전의 관리를 지원하는 도구는 requires-python 메타데이터가 정의되어 있다면 스크립트와 호환되는 사용 가능한 가장 높은 Python 버전을 사용하도록 시도해야 합니다.

도구 지원

다음은 이 PEP에 대한 지원을 표명했거나, 채택될 경우 지원을 구현하기로 약속한 도구의 목록입니다:

  • Pantsbuild and Pex: 의존성을 정의하는 어떤 방식이든 지원하며, 스크립트에서 패키지를 빌드하고 도구 구성을 포함하는 등 이 PEP가 유효한 사용 사례로 간주하는 기능도 지원한다고 표명했습니다.
  • MypyRuff: 도구 구성을 포함하면 사용자들이 겪고 있는 기존의 불편을 해결할 수 있으므로, 이에 대한 강력한 지지를 표명했습니다.
  • Hatch: (이 PEP의 작성자)는 이 PEP의 모든 측면을 지지한다고 표명했으며, 구체적으로 구성된 Python 버전으로 스크립트를 실행하는 것을 지원하는 최초의 도구 중 하나가 될 예정입니다.

거부된 아이디어

requirements.txt와 유사한 주석 블록을 사용하지 않는 이유는 무엇입니까?

이 PEP는 Python 코드가 단일 파일 스크립트로 존재하는 사용자를 여러 유형으로 구분합니다.

  • 특정 작업을 수행하기 위해 Python을 스크립팅 언어로만 사용하는 비프로그래머입니다. 이러한 사용자는 셰뱅 줄이나 PATH 환경 변수와 같은 운영 체제 개념에 익숙하지 않을 가능성이 높습니다. 몇 가지 예는 다음과 같습니다.
    • 효율성을 높이거나 지루한 작업을 줄이기 위해 무언가를 자동화하는 스크립트를 작성하려는 직장 등의 일반적인 사람입니다.
    • 일부 데이터를 분석하거나 연구 목적으로 스크립트를 작성하려는 산업계 또는 학계의 데이터 과학이나 머신 러닝 종사자입니다. 이러한 사용자는 프로그래밍 지식이 제한적이지만, StackOverflow나 프로그래밍 성향이 강한 블로그와 같은 출처에서 학습하며, 지식과 코드를 공유하는 커뮤니티에 속할 가능성도 점점 높아지고 있다는 점에서 특별합니다. 따라서 이러한 사용자 중 상당수는 Git(Hub), Jupyter, HuggingFace 등과 같은 것에 어느 정도 익숙할 것입니다.
  • 예를 들어 시스템 관리자인 운영 체제를 관리하는 비프로그래머입니다. 예를 들어 이러한 사용자는 PATH를 설정할 수 있지만, 가상 환경과 같은 Python 개념에는 익숙하지 않을 가능성이 높습니다. 이러한 사용자는 대개 독립적으로 작업하며 Git과 같이 공유를 목적으로 하는 도구를 접할 필요성이 제한적입니다.
  • 예를 들어 SRE와 같이 운영 체제 또는 인프라를 관리하는 프로그래머입니다. 이러한 사용자는 가상 환경과 같은 Python 개념에는 익숙하지 않을 가능성이 높지만, Git에는 익숙할 가능성이 높으며 Python 스크립트와 Kubernetes 구성 등 인프라 관리에 필요한 모든 것을 버전 관리하기 위해 Git을 사용하는 경우가 대부분입니다.
  • 주로 자신을 위해 스크립트를 작성하는 프로그래머입니다. 이러한 사용자는 시간이 지나면서 작업 흐름을 자동화하는 데 사용하는 다양한 언어의 스크립트를 매우 많이 축적하며, 지속성을 위해 버전 관리될 수도 있는 단일 디렉터리에 저장하는 경우가 많습니다. Windows가 아닌 운영 체제를 사용하는 사용자는 각 Python 스크립트에 원하는 Python 실행 파일이나 스크립트 실행기를 가리키는 셰뱅 줄을 설정할 수 있습니다.

이 PEP는 제안된 TOML 기반 메타데이터 형식이 각 사용자 범주에 가장 적합하며, requirements와 유사한 블록 주석은 requirements.txt에 익숙한 사용자만 쉽게 사용할 수 있고 이는 전체 사용자의 일부에 불과하다고 주장합니다.

  • 작업을 자동화하는 일반 사용자나 데이터 과학자는 이미 아무런 배경지식 없이 시작하며 TOML이나 requirements.txt에 익숙하지 않을 가능성이 높습니다. 이러한 사용자는 검색 엔진을 통해 온라인에서 찾은 코드 조각에 의존하거나, 챗봇 또는 직접 코드 완성 소프트웨어 형태의 AI를 사용할 가능성이 매우 높습니다. pyproject.toml에 저장된 의존성 정보와의 유사성은 비교적 빠르게 유용한 검색 결과를 제공할 것이며, pyproject.toml 형식과 스크립트 메타데이터 형식이 동일하지는 않더라도 그로 인해 발생하는 차이를 해당 사용자들이 해결하는 일은 어렵지 않을 가능성이 높습니다.

    또한 이러한 사용자는 서식상의 특이점과 구문 오류에 가장 취약합니다. TOML은 잘 정의된 형식으로, Python 표현식과 호환되는 대입 방식을 제공하는 기존 온라인 검증기가 있으며 엄격한 들여쓰기 규칙이 없습니다. 반면 블록 주석 형식은 예를 들어 콜론을 빠뜨리는 것만으로도 쉽게 잘못 작성될 수 있으며, 검색 엔진을 사용해 작동하지 않는 이유를 디버깅하는 일은 이러한 사용자에게 어려운 작업일 것입니다.

  • 시스템 관리자 유형의 사용자도 앞서 설명한 사용자들과 마찬가지로 TOML이나 requirements.txt에 익숙하지 않을 가능성이 높습니다. 어느 형식을 사용하든 문서를 읽어야 합니다. 이러한 사용자들은 구조화된 데이터 형식에 익숙하므로 TOML을 더 편하게 느낄 가능성이 높으며, 자신의 시스템에서 마법처럼 동작한다고 인식되는 부분도 줄어들 것입니다.

    또한 시스템을 유지 관리할 때 /// script는 시간이 지나며 수많은 확장이 추가될 수 있는 블록 주석보다 셸에서 훨씬 쉽게 검색할 수 있습니다.

  • SRE 유형의 사용자는 GitLab Runner 또는 Cloud Native Buildpacks를 구성하는 등, 작업해야 할 다른 프로젝트를 통해 이미 TOML에 익숙할 가능성이 높습니다.

    이러한 사용자는 자신의 시스템 보안을 책임지며, 의존성 버전을 업데이트하는 PR을 자동으로 열도록 보안 스캐너를 설정해 두었을 가능성이 높습니다. Dependabot과 같은 자동화 도구는 블록 주석 형식을 위한 자체 사용자 지정 파서를 작성하는 것보다 기존 TOML 라이브러리를 사용하는 편이 훨씬 수월합니다.

  • 프로그래머 유형의 사용자는 애플리케이션 작성 경험이 있는 Python 프로그래머가 아니라면 requirements.txt 파일을 본 적이 있더라도 TOML에 더 익숙할 가능성이 높습니다. requirements 형식을 사용해 본 경험이 있다는 것은 필연적으로 해당 생태계에 어느 정도 익숙하다는 의미이므로, TOML이 무엇인지 알고 있다고 가정해도 안전합니다.

    이러한 사용자에게 이 PEP가 제공하는 또 다른 이점은 Visual Studio Code와 같은 IDE가 각자 이 기능을 위한 사용자 지정 로직을 작성하는 것보다 훨씬 쉽게 TOML 구문 강조를 제공할 수 있다는 점입니다.

또한 원래의 블록 주석 대체 형식(이중 #)은 PEP 8의 권고에 어긋났으며, 그 결과 해당 권고를 준수하는 린터와 IDE 자동 포매터가 기본적으로 실패 했으므로, 최종 제안에서는 명확한 시작 또는 끝 시퀀스 없이 단일 # 문자로 시작하는 표준 주석을 사용합니다.

머신을 위한 것으로 보이지 않는 일반 주석(즉, 인코딩 선언이 동작에 영향을 미친다는 개념은 Python 사용자에게 일반적이지 않으며, “명시적인 것이 암시적인 것보다 낫다”라는 근본 원칙에 정면으로 어긋납니다.

사용자에게 산문처럼 보이는 내용을 입력하는 행위가 런타임 동작을 변경할 수 있습니다. 이 PEP는 도구가 그러한 방식으로 설정된 경우(시스템 관리자가 설정했을 수도 있습니다)에도 이러한 일이 발생할 가능성은 사용자에게 비우호적이라고 봅니다.

마지막으로, 그리고 결정적으로, PEP 722와 같은 이 PEP의 대안은 지원되는 Python 버전 설정, 스크립트를 궁극적으로 패키지로 빌드하는 작업, 사용자를 대신해 머신이 메타데이터를 편집할 수 있는 기능 등 여기에서 열거한 사용 사례를 충족하지 못합니다. 그러한 기능에 대한 요구가 계속될 가능성이 매우 높으며, 향후 다른 PEP에서 그러한 메타데이터의 삽입을 허용할 가능성도 생각해 볼 수 있습니다. 그렇게 되면 동일한 작업을 수행하는 방법이 여러 가지가 생기며, 이는 “이를 수행하는 명확한 방법은 하나, 가능하면 오직 하나만 있어야 한다”라는 우리의 근본 원칙에 어긋납니다.

다중 행 문자열을 사용하지 않는 이유는 무엇입니까?

이 PEP의 이전 버전에서는 메타데이터를 다음과 같이 저장하도록 제안했습니다.

__pyproject__ = """
...
"""

이 제안의 가장 중요한 문제는 내장된 TOML이 다음과 같은 방식으로 제한된다는 점입니다.

  • TOML에서 여러 줄의 큰따옴표 문자열을 사용할 수 없게 됩니다. 그렇게 하면 문서를 포함하는 Python 문자열과 충돌하기 때문입니다. 많은 TOML 작성 도구는 스타일을 보존하지 않으며, 잠재적으로 잘못된 형식의 출력을 생성할 수 있습니다.
  • Python 문자열에서 문자를 이스케이프하는 방식은 TOML 문자열에서 작동하는 방식과 완전히 같지 않습니다. 원시 문자열을 강제하면 일대일 문자 매핑을 보존할 수 있지만, 이 r 접두사 요구 사항은 사용자에게 혼란을 줄 수 있습니다.

핵심 메타데이터 필드를 재사용하지 않는 이유는 무엇입니까?

이 PEP의 이전 버전에서는 프로젝트를 설명하는 데 사용되는 기존 메타데이터 표준을 재사용하도록 제안했습니다.

이 제안에는 두 가지 중요한 문제가 있습니다.

특정 메타데이터 필드로 제한하지 않는 이유는 무엇입니까?

메타데이터를 dependencies로만 제한하면 Python 설치 관리를 지원하는 도구의 알려진 사용 사례를 막게 되며, 사용자가 새로운 구문이나 표준 라이브러리 기능을 위해 특정 Python 버전을 대상으로 지정할 수 없게 됩니다.

도구 구성으로 제한하지 않는 이유는 무엇입니까?

[tool]테이블을 허용하지 않으면 사용자에게 도움이 될 알려진 기능을 막게 됩니다. 예를 들어:

  • 스크립트 실행기는 내장된 잠금 파일에 대한 의존성 해결 데이터를 주입하는 기능을 지원할 수 있습니다(Go의 gorun이 할 수 있는 작업입니다).
  • 스크립트 실행기는 의존성에 대한 플랫폼 간 지원이 없거나, Nvidia 드라이버가 필요한 경우처럼 일반 사용자에게 설정이 너무 복잡한 상황에서 컨테이너로 스크립트를 실행하도록 지시하는 구성을 지원할 수 있습니다. 이러한 상황에서는 사용자가 원하는 작업을 계속 진행할 수 있지만, 그렇지 않으면 그 시점에서 작업을 완전히 중단할 수도 있습니다.
  • 도구는 단일 파일 스크립트를 패키지로 빌드하는 것처럼 사용자의 개발 부담을 줄이는 기능을 실험하고자 할 수 있습니다. 단일 파일에서 휠과 소스 배포판을 빌드하는 도구가 이미 실제로 존재한다는 피드백를 받았습니다.

    메타데이터 임베딩에 관한 Rust RFC의 작성자는 소규모 프로젝트 관리에 불필요한 마찰이 있다는 사용자 피드백을 바탕으로 자신들도 이를 적극적으로 검토하고 있다고 우리에게 언급했습니다.

    최소 하나의 주요 빌드 시스템이 이를 지원하겠다는 약속을 했습니다.

도구의 동작을 제한하지 않는 이유는 무엇입니까?

이 PEP의 이전 버전에서는 스크립트가 도구에 대한 유일한 입력이 아닐 때, 스크립트를 실행하지 않는 도구는 동작을 변경해서는 안 된다고 SHOULD NOT 제안했습니다. 예를 들어 린터가 디렉터리 경로와 함께 호출되면, 내장된 메타데이터가 있는 파일이 하나도 없는 경우와 동일하게 동작해야 한다고 SHOULD 명시했습니다.

이는 도구 동작의 혼란과 이 PEP를 지원하도록 도구에 다양한 기능 요청이 생성되는 것을 방지하기 위한 예방 조치로 수행되었습니다. 그러나 논의 중에 이러한 방식이 바람직하지 않고 사용자에게 혼란을 줄 가능성이 있다는 도구 유지 관리자들의 피드백을 받았습니다. 또한 이는 특정 상황에서 도구를 보편적으로 더 쉽게 구성하고 기존 문제를 해결하는 방법을 제공할 수도 있습니다.

pyproject.toml을 사용하여 Python 프로젝트를 설정하면 되지 않는 이유는 무엇입니까?

다시 말해, 여기서 핵심 문제는 이 제안의 대상 사용자가 배포를 목적으로 하지 않는 스크립트를 작성하는 사람들이라는 점입니다. 때때로 스크립트가 “공유”되기도 하지만, 이는 “배포”보다 훨씬 비공식적이며 일반적으로 스크립트 실행 방법을 설명하는 간단한 지침과 함께 이메일로 스크립트를 보내거나, 누군가에게 GitHub gist 링크를 전달하는 방식으로 이루어집니다.

이러한 사용자에게 Python 패키징의 복잡성을 배우도록 요구하는 것은 복잡성을 크게 높이는 일이며, 거의 확실히 “Python은 스크립트에 너무 어렵다”는 인상을 줄 것입니다.

또한 여기서 pyproject.toml이 어떤 방식으로든 현재 위치에서 스크립트를 실행하도록 설계될 것이라는 기대가 있다면, 이는 현재 존재하지 않는 표준의 새로운 기능입니다. 최소한, 휠로 배포되지 않을 프로젝트에서 pyproject.toml을 사용하는 것에 관한 Discourse의 현재 논의 가 해결되기 전까지는 합리적인 제안이 아닙니다. 그리고 그 이후에도 gist나 이메일로 누군가에게 스크립트를 보내는 사용 사례는 해결하지 못합니다.

import 문에서 요구 사항을 추론하면 되지 않는 이유는 무엇입니까?

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

그러나 이는 여러 가지 이유로 실행 불가능합니다. 첫째, 모든 Python 버전에서, 그리고 다른 언어로 작성된 도구에서도 구문을 쉽게 구문 분석할 수 있도록 유지해야 한다는 위의 지적은 여기에도 동일하게 적용됩니다.

둘째, PyPI와 Simple Repository API를 준수하는 기타 패키지 저장소는 가져온 모듈 이름에서 패키지 이름을 확인할 수 있는 메커니즘을 제공하지 않습니다(이 관련 논의도 참조하십시오).

셋째, 저장소에서 이 정보를 제공하더라도 동일한 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께 감사드립니다.)

종속성에 requirements 파일을 사용하면 안 됩니까?

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

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

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

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

여기서 “명백한” 해결책은 파일 이름을 스크립트와 동일하게 지정하되 .reqs확장자(또는 이와 유사한 것)를 사용하는 것입니다. 그러나 이렇게 하면 현재는 파일 하나만 필요했던 상황에서 여전히 two개의 파일이 필요하며, 따라서 “더 나은 배치 파일” 모델과 일치하지 않습니다(셸 스크립트와 배치 파일은 일반적으로 자체적으로 완결되어 있습니다). 개발자가 두 파일을 함께 보관해야 한다는 점을 기억해야 하며, 이것이 항상 가능하지는 않을 수 있습니다. 예를 들어 시스템 관리 정책에 따라 특정 디렉터리의 all 파일이 실행 가능해야 할 수 있습니다(예를 들어 Linux 파일 시스템 표준에서는 /usr/bin에 대해 이를 요구합니다). 또한 스크립트를 공유하는 일부 방법(예를 들어 Github의 gist와 같은 텍스트 파일 공유 서비스나 기업 인트라넷에 게시하는 방법)에서는 스크립트의 위치로부터 관련 requirements 파일의 위치를 도출하지 못할 수 있습니다(pipx와 같은 도구는 URL에서 직접 스크립트를 실행할 수 있으므로 “스크립트와 종속성의 zip 파일을 다운로드하여 압축을 해제하는 것”이 적절한 요구 사항이 아닐 수 있습니다).

본질적으로 여기서의 문제는 형식이 종속성 데이터를 in the script file itself에 저장하는 것을 지원해야 한다는 요구 사항이 명시적으로 제시되어 있다는 점입니다. 그렇게 하지 않는 해결책은 단순히 해당 요구 사항을 무시하는 것입니다.

(어쩌면 제한된) Python 구문을 사용하면 안 됩니까?

일반적으로 이는 다음과 같은 여러 특수 변수에 메타데이터를 저장하는 방식이 됩니다.

__requires_python__ = ">=3.11"
__dependencies__ = [
    "requests",
    "click",
]

이 제안의 가장 큰 문제는 종속성 데이터를 사용하는 모든 소비자가 Python 구문 분석기를 구현해야 한다는 점입니다. 구문이 제한되어 있더라도 스크립트의 rest 부분에서는 전체 Python 구문을 사용하게 되며, 주변 코드와 분리된 상태에서 성공적으로 구문 분석할 수 있는 구문을 정의하려는 시도는 매우 어렵고 오류가 발생하기 쉬울 가능성이 높습니다.

또한 Python의 구문은 릴리스마다 변경됩니다. 종속성 데이터를 추출하는 데 Python 파서가 필요하다면, 해당 파서는 스크립트가 어떤 Python 버전용으로 작성되었는지 알아야 하며, multiple Python 버전을 처리할 수 있는 파서를 갖춘 범용 도구의 오버헤드는 감당할 수 없습니다.

이 접근법에서는 새로운 확장이 추가될수록 스크립트가 많은 변수로 복잡해질 가능성이 있습니다. 또한 어떤 메타데이터 필드가 어떤 변수 이름에 대응하는지 직관적으로 파악해야 하므로 사용자에게 혼란을 일으킬 수 있습니다.

다만 pip-run유틸리티가 이 접근법을 (확장된 형태로) 구현한다는 점에 유의할 필요가 있습니다. 프로젝트의 이슈 트래커에서 pip-run설계에 관한 Further discussion를 확인할 수 있습니다.

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

특수한 메타데이터와 도구가 필요하지 않으며, 단순히 의존성의 위치를 sys.path에 추가하여 처리할 수 있습니다. 이 경우에는 이 PEP가 전혀 필요하지 않습니다. 반면 “로컬 의존성”이 로컬에서 게시된 실제 배포판이라면 일반적인 방식으로 PEP 508 요구 사항으로 지정할 수 있으며, 도구를 실행할 때 해당 도구의 UI를 사용하여 로컬 패키지 색인을 지정할 수 있습니다.

미해결 문제

현재로서는 없습니다.

참고 문헌

각주