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는 단일 파일 Python 스크립트에 포함할 수 있는 메타데이터 형식을 규정합니다. 이 형식은 이러한 스크립트와 상호 작용해야 할 수 있는 런처, IDE 및 기타 외부 도구를 돕기 위한 것입니다.

동기

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 [1]에 대응하여 CI에서 실행되는 의존성 업데이트 자동화가 있습니다.

명세

이 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 유형

첫 번째 메타데이터 블록 유형의 이름은 script이며, 이 블록은 스크립트 메타데이터(의존성 데이터와 도구 설정)를 담습니다.

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

[tool] 테이블은 스크립트 실행기이든 아니든 어떤 도구라도 동작을 설정하는 데 사용할 수 있습니다. 이 테이블은 pyproject.toml 파일의 도구 테이블과 같은 의미를 가집니다.

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

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

스크립트 실행기는 지정된 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)
        )

하위 호환성

이 글을 쓰는 시점에 # /// script 블록 주석 시작 표시는 GitHub에 있는 어떤 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 버전의 환경을 제공할 수 있는 것은 아닙니다.

도구 테이블은 스크립트 실행기든 아니든 어떤 도구라도 동작을 구성하는 데 사용할 수 있습니다.

권장 사항

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

도구 지지 현황

다음은 이 PEP에 대한 지지를 표명했거나, 이 PEP가 수락되면 지원을 구현하기로 약속한 도구의 목록입니다:

  • Pantsbuild 및 Pex: 의존성을 정의하는 모든 방식에 대한 지지를 표명했으며, 스크립트로부터 패키지를 빌드하는 것과 도구 설정을 내장하는 것처럼 이 PEP가 유효한 사용 사례로 간주하는 기능에 대해서도 지지를 표명했습니다.
  • Mypy와 Ruff: 도구 설정을 내장하면 사용자들이 현재 겪고 있는 불편이 해결되므로 이에 대한 강력한 지지를 표명했습니다.
  • Hatch: (이 PEP의 작성자) 이 PEP의 모든 측면에 대한 지지를 표명했으며, 특정하게 구성된 Python 버전으로 스크립트를 실행하는 기능을 가장 먼저 지원하는 도구 중 하나가 될 것입니다.

기각된 아이디어

왜 requirements.txt와 비슷한 주석 블록을 사용하지 않습니까?

이 PEP는 Python 코드가 단일 파일 스크립트 형태로 존재하게 되는 사용자 유형이 여러 가지 있다고 봅니다:

  • 특정 작업을 수행하기 위해 Python을 스크립팅 언어로만 사용하는 비프로그래머입니다. 이러한 사용자는 셔뱅 줄이나 PATH 환경 변수와 같은 운영 체제 개념에 익숙하지 않을 가능성이 높습니다. 몇 가지 예는 다음과 같습니다.
    • 효율성을 높이거나 지루한 일을 줄이기 위해 무언가를 자동화하는 스크립트를 작성하려는, 아마도 직장에 있는 평범한 사람입니다.
    • 산업계나 학계에서 데이터 과학이나 머신 러닝을 하면서, 데이터를 분석하거나 연구 목적으로 스크립트를 작성하려는 사람입니다. 이러한 사용자는 프로그래밍 지식이 제한적이지만 StackOverflow나 프로그래밍 성향의 블로그 같은 자료로부터 배우며, 지식과 코드를 공유하는 커뮤니티에 참여할 가능성이 점점 높아지고 있다는 점에서 특별합니다. 따라서 이러한 사용자 중 적지 않은 수가 Git(Hub), Jupyter, HuggingFace 등에 어느 정도 익숙할 것입니다.
  • 시스템 관리자처럼 운영 체제를 관리하는 비프로그래머입니다. 예를 들어 이러한 사용자는 PATH를 설정할 수는 있지만, 가상 환경과 같은 Python 개념에는 익숙하지 않을 가능성이 높습니다. 이러한 사용자는 흔히 고립된 환경에서 일하며, Git처럼 공유를 위한 도구를 접할 필요가 별로 없습니다.
  • SRE처럼 운영 체제/인프라를 관리하는 프로그래머입니다. 이러한 사용자는 가상 환경과 같은 Python 개념에는 그다지 익숙하지 않을 가능성이 높지만, Git에는 익숙할 가능성이 높으며 대개 Git을 사용해 Python 스크립트와 Kubernetes 설정처럼 인프라 관리에 필요한 모든 것을 버전 관리합니다.
  • 주로 자기 자신을 위해 스크립트를 작성하는 프로그래머입니다. 이러한 사용자는 시간이 지나면서 워크플로를 자동화하는 데 사용하는 다양한 언어의 스크립트를 상당히 많이 축적하며, 이를 하나의 디렉터리에 저장하는 경우가 많습니다. 이 디렉터리는 영속성을 위해 버전 관리될 수도 있습니다. 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 라이브러리를 사용하는 편이 훨씬 수월합니다.

  • 프로그래머 유형은 requirements.txt 파일을 본 적이 있을 가능성보다 TOML에 익숙할 가능성이 더 높습니다. 다만 애플리케이션을 작성해 본 경험이 있는 Python 프로그래머라면 예외입니다. 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] 테이블을 허용하지 않으면 사용자에게 도움이 될, 이미 알려진 기능을 막게 됩니다. 예를 들면 다음과 같습니다:

  • 스크립트 실행기는 내장된 잠금 파일(lock file)을 위한 의존성 해석 데이터 주입을 지원할 수 있습니다(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되는 모듈 이름으로부터 패키지 이름을 알아내는 메커니즘을 제공하지 않습니다(이 관련 논의도 참고하십시오).

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

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

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

따라서 표준을 만들려면 두 가지가 필요할 것입니다:

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

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

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

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

왜 (제한될 수도 있는) Python 문법을 사용하지 않습니까?

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

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

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

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

이 접근 방식에서는 새로운 확장이 추가될수록 스크립트가 많은 변수로 어지러워질 가능성이 있습니다. 또한 어떤 메타데이터 필드가 어떤 변수 이름에 대응하는지 짐작해야 하므로 사용자에게 혼란을 줄 것입니다.

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

로컬 의존성은 어떻게 합니까?

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

미해결 문제

현재로서는 없습니다.

참고 문헌

각주