PEP 662 – 가상 휠을 통한 편집 가능 설치
- Author:
- Bernát Gábor <gaborjbernat at gmail.com>
- Sponsor:
- Brett Cannon <brett at python.org>
- Discussions-To:
- Discourse thread
- Status:
- Rejected
- Type:
- Standards Track
- Topic:
- Packaging
- Created:
- 28-May-2021
- Post-History:
- Resolution:
- Discourse thread
번역·라이선스 안내
이 비공식 한국어 번역은 원문 Copyright 절의 Public Domain or CC0-1.0, whichever is more permissive 조건에 따라 제공합니다. 원저자와 공식 원문은 그대로 표시합니다. 수정되지 않은 기준 원문 · 공식 최신판
초록
이 문서는 가상 휠을 도입하여 프로젝트를 편집 가능 모드로 설치할 수 있도록, (PEP 517에서 도입된) 빌드 백엔드와 프런트엔드 간 통신의 확장을 설명합니다.
동기
개발 중에 많은 Python 사용자는 추가 설치 단계 없이도 기반 소스 코드와 리소스의 변경 사항이 이후의 인터프리터 호출에 자동으로 반영되도록 라이브러리를 설치하는 것을 선호합니다. 이 모드는 보통 “개발 모드” 또는 “편집 가능 설치”라고 부릅니다. 실제로 관찰되는 동작이 복잡하다는 이유로 PEP 517에서 명시적으로 제외되었기 때문에, 현재는 이를 수행하는 표준화된 방법이 없습니다.
현재 사용자는 이러한 동작을 얻기 위해 다음 중 하나를 수행합니다:
- Python 코드만 다루는 경우에는 관련 소스 디렉터리를
sys.path에 추가합니다(명령줄 인터페이스에서는PYTHONPATH환경 변수로 설정할 수 있습니다). 이 경우 사용자가 프로젝트 의존성을 직접 설치해야 하며, 진입점이나 프로젝트 메타데이터는 생성되지 않는다는 점에 유의하십시오. - setuptools는 setup.py develop 메커니즘을 제공합니다. 이 메커니즘은 인터프리터 시작 시점에 프로젝트 루트를
sys.path에 주입하는pth파일을 설치하고, 프로젝트 메타데이터를 생성하며, 프로젝트 의존성도 설치합니다. pip는 pip install -e 명령줄 인터페이스를 통해 이 메커니즘을 호출할 수 있게 합니다. - flit은 프로젝트 파일을 인터프리터의
purelib폴더에 심볼릭 링크로 연결하고, 프로젝트 메타데이터를 생성하며, 의존성도 설치하는 flit install –symlink 명령을 제공합니다. 이 방식은 리소스 파일도 지원할 수 있다는 점에 유의하십시오.
이 예시들에서 보듯이 편집 가능 설치는 여러 방식으로 구현할 수 있으며, 현재는 이를 수행하는 표준 방식이 없습니다. 게다가 편집 가능 설치가 무엇인지 정의하고 이를 구현하는 일이 누구의 책임인지도 명확하지 않습니다:
- 빌드 백엔드가 이를 정의하고 구체화하도록 하는 방법,
- 빌드 프런트엔드가 이를 정의하고 구체화하도록 하는 방법,
- 가능한 선택지 중 하나의 방법을 명시적으로 정의하고 표준화하는 방법.
이 PEP의 저자는 여기에 모든 경우에 들어맞는 해법은 없으며, 편집 가능 효과를 얻는 각 방법에는 장단점이 있다고 생각합니다. 따라서 이 PEP는 커뮤니티가 단일 해법에 합의할 가능성이 낮으므로 세 번째 선택지를 기각합니다. 또한 프런트엔드와 빌드 백엔드 중 어느 쪽이 이 책임을 맡아야 하는지에 대한 문제가 남아 있습니다. PEP 660은 빌드 백엔드가 이를 맡도록 제안하는 반면, 이 PEP는 주로 프런트엔드가 맡도록 제안하되 백엔드가 원한다면 제어권을 가져갈 수 있도록 여전히 허용합니다.
근거
PEP 517은 “편집 가능 설치”를 연기했습니다. 이를 포함하면 채택이 더 늦어졌을 것이고, 편집 가능 설치를 어떻게 구현해야 하는지에 대한 합의도 없었기 때문입니다. setuptools와 pip 프로젝트의 인기로 인해 현상 유지가 이어졌고, 백엔드는 setup.py develop 구현을 제공하여 편집 가능 모드를 구현할 수 있었으며, 사용자는 이를 pip install -e를 통해 실행할 수 있었습니다. 빌드 백엔드와 프런트엔드 사이의 편집 가능 인터페이스를 정의함으로써 setup.py 파일과 현재의 통신 방식을 없앨 수 있습니다.
용어와 목표
이 PEP는 프런트엔드와 백엔드의 역할을 명확히 구분하고, 각각의 개발자가 사용자에게 가치 있는 기능을 제공할 수 있는 역량을 최대한 부여하는 것을 목표로 합니다. 이 제안에서 백엔드의 역할은 편집 가능 설치를 위해 프로젝트를 준비한 다음, 프런트엔드가 편집 가능 설치를 구체화하고 적용할 수 있도록 충분한 정보를 프런트엔드에 제공하는 것입니다.
백엔드가 프런트엔드에 제공하는 정보는 PEP 427의 기존 명세를 따르는 휠입니다. 아카이브 자체에 대한 휠 메타데이터({distribution}-{version}.dist-info/WHEEL)에는 값이 true인 Editable 키도 반드시 포함되어야 합니다.
다만 휠 안에 프로젝트 파일을 제공하는 대신, 프런트엔드가 노출할 파일을 정의하는 editable.json 파일을 (휠의 루트 수준에) 반드시 제공해야 합니다. 이 파일의 내용은 스킴 매핑 안에서 절대 소스 트리 경로를 대상 인터프리터의 상대 목적지 경로로 대응시키는 매핑으로 구성됩니다.
앞의 두 단락을 충족하는 휠이 가상 휠입니다. 프런트엔드의 역할은 가상 휠을 받아 프로젝트를 편집 가능 모드로 설치하는 것입니다. 이를 수행하는 방식은 전적으로 프런트엔드에 달려 있으며 구현 세부 사항으로 간주됩니다.
편집 가능 설치 모드는 설치되는 프로젝트의 소스 코드를 로컬 디렉터리에서 사용할 수 있음을 전제합니다. 프로젝트가 편집 가능 모드로 설치되면, 로컬 소스 트리에서 프로젝트 코드를 일부 변경하면 새로운 설치 단계 없이도 반영됩니다. 최소한, 설치 시점에 존재하던 생성되지 않은 파일의 텍스트를 변경하면 이후 패키지를 임포트할 때 반영되어야 합니다.
진입점을 추가하거나 수정하는 것, 새 의존성을 추가하는 것과 같은 일부 종류의 변경 사항은 새로운 설치 단계를 거쳐야 반영됩니다. 이러한 변경은 일반적으로 빌드 백엔드 설정 파일(pyproject.toml 등)에서 이루어집니다. 이 요구 사항은 이러한 수정 사항이 재설치 후에야 반영될 것이라는 일반적인 사용자 기대와 일치합니다.
사용자는 편집 가능 설치가 표준 설치와 동일하게 동작하기를 기대하지만, 이것이 항상 가능한 것은 아니며 다른 사용자 기대와 상충할 수도 있습니다. 프런트엔드가 편집 가능 모드를 구현하는 방식에 따라, 소스 트리나 인터프리터의 설치 경로에 (일반적인 설치와 비교하여) 추가 파일이 존재하는 것과 같은 일부 차이가 드러날 수 있습니다.
프런트엔드는 편집 가능 설치와 표준 설치 간의 동작 차이를 최소화하도록 노력해야 하며, 알려진 차이점을 문서화해야 합니다.
참고로, 편집 가능하지 않은 설치는 다음과 같이 동작합니다:
- 개발자는 프로젝트 개발을 진행하기 위해 도구(예: pip)를 사용하며, 여기서는 이를 프런트엔드라고 부르겠습니다. 사용자가 프로젝트의 패키지 빌드와 설치를 시작하려 할 때, 사용자는 프런트엔드와 소통합니다.
- 프런트엔드는 빌드 프런트엔드(예: build)를 사용하여 휠 빌드를 시작합니다. 빌드 프런트엔드는 PEP 517을 사용하여 빌드 백엔드(예: setuptools)와 소통하며, 이때 빌드 백엔드는 PEP 518 환경에 설치되어 있습니다. 호출되면 백엔드는 휠을 반환합니다.
- 프런트엔드는 휠을 받아 설치 프로그램(예: installer)에 전달하여 대상 Python 인터프리터에 휠을 설치합니다.
메커니즘
이 PEP는 PEP 517 백엔드 인터페이스에 두 개의 선택적 훅을 추가합니다. 훅 중 하나는 편집 가능 설치의 빌드 의존성을 지정하는 데 사용됩니다. 다른 하나의 훅은 프런트엔드가 편집 가능 설치를 만드는 데 필요한 정보를 빌드 프런트엔드를 통해 반환합니다.
get_requires_for_build_editable
def get_requires_for_build_editable(config_settings=None):
...
이 훅은 pyproject.toml 파일에 지정된 것에 더하여, PEP 508 의존성 명세를 담은 추가 문자열 시퀀스를 반환해야 합니다(MUST). 프런트엔드는 build_editable 훅이 호출되는 빌드 환경에서 이러한 의존성을 사용할 수 있도록 보장해야 합니다.
정의되지 않은 경우, 기본 구현은 []를 반환하는 것과 동일합니다.
prepare_metadata_for_build_editable
def prepare_metadata_for_build_editable(metadata_directory, config_settings=None):
...
지정된 metadata_directory 안에 휠 메타데이터를 담은 .dist-info 디렉터리를 생성해야 합니다(즉, {metadata_directory}/{package}-{version}.dist-info/와 같은 디렉터리를 생성합니다). 이 디렉터리는 휠 명세에 정의된 유효한 .dist-info 디렉터리여야 합니다(MUST). 단, RECORD나 서명은 포함하지 않아도 됩니다. 훅은 이 디렉터리 안에 다른 파일을 생성할 수도 있으며(MAY), 빌드 프런트엔드는 그러한 파일을 보존하되 그 외에는 무시해야 합니다(MUST). 이는 메타데이터가 빌드 시점의 결정에 의존하는 경우, 빌드 백엔드가 실제 휠 빌드 단계에서 재사용할 수 있도록 이러한 결정을 편리한 형식으로 기록해야 할 수도 있기 때문입니다.
이 훅은 생성한 .dist-info 디렉터리의 기본 이름(전체 경로가 아님)을 유니코드 문자열로 반환해야 합니다.
빌드 프런트엔드에 이 정보가 필요한데 해당 메서드가 정의되어 있지 않다면, build_editable을 호출하고 그 결과로 생성된 메타데이터를 직접 확인해야 합니다.
build_editable
def build_editable(self, wheel_directory, config_settings=None,
metadata_directory=None):
...
.whl 파일을 빌드하여 지정된 wheel_directory에 배치해야 합니다. 생성한 .whl 파일의 기본 이름(전체 경로가 아님)을 유니코드 문자열로 반환해야 합니다. wheel 파일은 용어 절에서 정의한 가상 휠 유형이어야 합니다.
빌드 프런트엔드가 이전에 prepare_metadata_for_build_editable을 호출했고, 이번 호출로 생성되는 휠의 메타데이터가 앞선 호출의 결과와 일치해야 한다면, 생성된 .dist-info 디렉터리의 경로를 metadata_directory 인자로 제공해야 합니다. 이 인자가 제공되면 build_editable은 반드시 동일한 메타데이터를 가진 휠을 생성해야 합니다. 빌드 프런트엔드가 전달하는 디렉터리는 반드시 prepare_metadata_for_build_editable이 생성한 디렉터리와 동일해야 하며, 여기에는 이 훅이 생성한 인식되지 않는 파일도 모두 포함됩니다.
prepare_metadata_for_build_editable 훅을 제공하지 않는 백엔드는 build_editable의 metadata_directory 매개변수를 조용히 무시하거나, 이 매개변수가 None 이외의 값으로 설정되었을 때 예외를 발생시킬 수 있습니다.
소스 디렉터리는 읽기 전용일 수 있으며, 이런 경우 백엔드는 프런트엔드가 사용자에게 표시할 수 있는 오류를 발생시킬 수 있습니다. 백엔드는 중간 산출물을 캐시 위치나 임시 디렉터리에 저장할 수 있습니다. 캐시의 유무가 빌드의 최종 결과에 실질적인 차이를 만들어서는 안 됩니다.
editable.json의 내용은 반드시 다음 JSON 스키마 검사를 통과해야 합니다:
{
"$schema": "http://json-schema.org/draft-07/schema",
"$id": "http://pypa.io/editables.json",
"type": "object",
"title": "Virtual wheel editable schema.",
"required": ["version", "scheme"],
"properties": {
"version": {
"$id": "#/properties/version",
"type": "integer",
"minimum": 1,
"maximum": 1,
"title": "The version of the schema."
},
"scheme": {
"$id": "#/properties/scheme",
"type": "object",
"title": "Files to expose.",
"required": ["purelib", "platlib", "data", "headers", "scripts"],
"properties": {
"purelib": { "$ref": "#/$defs/mapping" },
"platlib": { "$ref": "#/$defs/mapping" },
"data": { "$ref": "#/$defs/mapping" },
"headers": { "$ref": "#/$defs/mapping" },
"scripts": { "$ref": "#/$defs/mapping" }
},
"additionalProperties": true
}
},
"additionalProperties": true,
"$defs": {
"mapping": {
"type": "object",
"description": "A mapping of source to target paths. The source is absolute path, the destination is relative path.",
"additionalProperties": true
}
}
}
예를 들면 다음과 같습니다:
{
"version": 1,
"scheme": {
"purelib": {"/src/tree/a.py": "tree/a.py"},
"platlib": {},
"data": {"/src/tree/py.typed": "tree/py.typed"},
"headers": {},
"scripts": {}
}
}
스킴 경로는 프로젝트 소스의 절대 경로를 대상 디렉터리의 상대 경로로 매핑합니다. 백엔드는 이 매핑을 사용하여 프로젝트 소스 디렉터리의 프로젝트 레이아웃을 인터프리터가 보게 될 형태로 바꿀 수 있습니다.
예를 들어 백엔드가 "purelib": {"/me/project/src": ""}를 반환하면, /me/project/src 안의 모든 파일과 모듈을 대상 인터프리터의 purelib 경로 루트에 노출한다는 뜻입니다.
빌드 프런트엔드 요구 사항
빌드 프런트엔드는 빌드 백엔드가 가상 휠을 생성할 수 있도록 환경을 구성할 책임이 있습니다. 휠 빌드 훅에 대한 PEP 517의 모든 권고 사항이 여기에도 적용됩니다.
프런트엔드 요구 사항
프런트엔드는 가상 휠을 PEP 427에 정의된 그대로 설치해야 합니다. 또한 editable.json 파일에 정의된 파일들도 설치할 책임이 있습니다. 설치 방식은 프런트엔드에 맡기며, 프런트엔드는 어떤 방식을 선택했는지와 그 방식에 어떤 제약이 있는지를 사용자에게 정확히 알리는 것이 권장됩니다.
프런트엔드는 PEP 610에 따라 설치된 배포판의 .dist-info 디렉터리에 direct_url.json 파일을 생성해야 합니다. url 값은 프로젝트 디렉터리(즉, pyproject.toml을 포함하는 디렉터리)를 가리키는 file:// URL이어야 하며, dir_info 값은 {'editable': true}이어야 합니다.
프런트엔드는 편집 가능 모드로 설치할 때 prepare_metadata_for_build_editable 훅에 의존할 수 있습니다.
빌드 백엔드가 제공한 정보로는 편집 가능 설치를 할 수 없다고 판단되면, 프런트엔드는 실패 처리하고 그 이유를 사용자에게 명확히 알리는 오류를 발생시켜야 합니다.
프런트엔드는 하나 이상의 편집 가능 설치 메커니즘을 구현할 수 있으며, 사용자의 사용 사례에 가장 적합한 메커니즘을 사용자가 선택하도록 맡길 수 있습니다. 예를 들어 pip는 편집 가능 모드 플래그를 추가하여 사용자가 pth 파일과 심볼릭 링크 중에서 선택하도록 할 수 있습니다( pip install -e . --editable-mode=pth 대 pip install -e . --editable-mode=symlink).
편집 가능 설치 구현 예시
이 PEP가 어떻게 사용될 수 있는지 보여 드리기 위해 이제 몇 가지 사례 연구를 제시합니다. 제시된 해결책은 순전히 설명을 위한 것이며 프런트엔드/백엔드에 대한 규범이 아니라는 점에 유의하십시오.
소스 트리를 있는 그대로 인터프리터에 추가하기
이는 가장 단순한 구현 중 하나로, 소스 트리를 있는 그대로 인터프리터의 스킴 경로에 추가합니다. 가상 휠 안의 editable.json은 다음과 같은 모습일 수 있습니다:
{
{"version": 1, "scheme": {"purelib": {"<project dir>": "<project dir>"}}}
}
그러면 프런트엔드는 다음 중 하나를 수행할 수 있습니다:
- 대상 인터프리터가 시작될 때 소스 디렉터리를 그 인터프리터의
sys.path에 추가합니다. 이는 대상 인터프리터의purelib폴더에pth파일을 만들어서 이루어집니다. 현재 setuptools가 이 방식을 사용하며, pip install -e도 이 방식으로 변환됩니다. 이 해결책은 빠르고 여러 플랫폼에서 호환됩니다. 그러나 이 방식은 소스 트리 전체를 시스템에 올려놓으므로, 표준 설치 시에는 사용할 수 없었을 모듈이 노출될 가능성이 있습니다. - 폴더 또는 그 안의 개별 파일에 대한 심볼릭 링크를 만듭니다. 이 방법은 flit이 flit install –symlink을 통해 사용하는 방법입니다. 이 해결책을 사용하려면 현재 플랫폼이 심볼릭 링크를 지원해야 합니다. 그래도 개별 파일에 대한 심볼릭 링크를 만들 수 있으므로, 소스 트리에서 제외되어야 할 파일이 포함되는 문제를 해결할 수 있습니다.
사용자 정의 임포터 사용하기
빌드 백엔드와 대상 인터프리터 사이의 더 견고하고 더 동적인 협업을 위해, 사용자 정의 임포터의 등록을 허용하는 임포트 시스템을 활용할 수 있습니다. 자세한 내용은 PEP 302를, 이에 대한 예로는 editables를 참조하십시오. 백엔드는 편집 가능 빌드 중에 새 임포터를 생성하고(또는 추가 의존성으로 설치하고), pth 파일을 추가하여 인터프리터 시작 시 이를 등록할 수 있습니다.
{
"version": 1,
"scheme": {
"purelib": {
"<project dir>/.editable/_register_importer.pth": "<project dir>/_register_importer.pth".
"<project dir>/.editable/_editable_importer.py": "<project dir>/_editable_importer.py"
}
}
}
}
여기서 백엔드는 새 모듈이 임포트될 때마다 호출되는 훅을 등록하여, 동적이고 필요에 따라 동작하는 기능을 가능하게 합니다. 이것이 유용한 잠재적 사용 사례는 다음과 같습니다:
- 소스 폴더를 노출하되 모듈 제외 설정을 준수하기: 백엔드는 소스 파일 로더가 소스 디렉터리에서 파일을 발견하도록 허용할지 여부를 결정하기 전에 제외 테이블을 참조하는 임포트 훅을 생성할 수 있습니다.
- 어떤 프로젝트에
A.py와B.py라는 두 모듈이 있다고 합시다. 이 둘은 소스 디렉터리에서는 별개의 두 파일이지만, 휠을 빌드하는 동안 하나의 거대한 파일project.py로 병합됩니다. 이 경우 이 PEP를 사용하면, 백엔드는 임포트 시점에 소스 파일들을 읽고 메모리에서 병합한 다음 모듈로 구체화하는 임포트 훅을 생성할 수 있습니다. - 오래된 C 확장을 자동으로 갱신하기: 백엔드는 C 확장 소스 파일의 마지막 수정 타임스탬프를 확인하는 임포트 훅을 생성할 수 있습니다. 그 값이 현재 C 확장 바이너리보다 크면, 임포트 전에 컴파일러를 호출하여 갱신을 수행합니다.
기각된 아이디어
이 PEP는 PEP 660과 경쟁하며, 편집 가능 설치를 달성하는 메커니즘이 빌드 백엔드가 아니라 프런트엔드 안에 있어야 한다고 생각하기 때문에 그 제안을 거부합니다. 게다가 이 접근 방식은 생태계가 편집 가능 설치 효과를 얻기 위해 다른 수단을 사용할 수 있도록 합니다(예: 해당 PEP가 설명하는 백엔드의 느슨한 휠 모드를 그대로 따르는 대신 sys.path에 경로를 삽입하거나 심볼릭 링크를 사용하는 방법).
무엇보다도 PEP 660은 wheel 파일 표준에 심볼릭 링크 지원을 함께 추가하지 않고서는 코드 및 데이터 파일을 노출하는 데 심볼릭 링크를 사용하는 것을 허용하지 않습니다. 휠 자체 안의 파일이 아니라 로컬 디스크에서만 사용할 수 있는 파일을 가리키는 심볼릭 링크를 지원하도록 휠 형식을 어떻게 확장할 수 있을지는 분명하지 않습니다. 백엔드 자체(또는 백엔드가 생성한 코드)가 (예를 들어 인터프리터 시작 시점에) 이러한 심볼릭 링크를 생성해서는 안 된다는 점에 유의해야 합니다. 그렇게 하면 어떤 파일을 제거해야 하는지에 대한 프런트엔드의 기록 관리와 충돌하기 때문입니다.
마지막으로, PEP 660은 purelib 및 platlib 파일에 대한 지원만 추가합니다. 이 PEP는 wheel 형식이 지원하는 다른 종류의 정보인 include, data, scripts는 의도적으로 지원하지 않습니다. 이 방식을 사용하면 프런트엔드는 심볼릭 링크 메커니즘을 통해 이들을 최선의 노력(best effort) 수준으로 지원할 수 있습니다(다만 이 기능을 모든 환경에서 사용할 수 있는 것은 아니며, Windows에서는 별도로 활성화해야 합니다). 저희는 이러한 파일 유형을 지원할 가능성 자체를 배제하기보다는 최선의 노력 수준의 지원을 추가하는 것이 유익하다고 생각합니다.
참고 문헌
Copyright
This document is placed in the public domain or under the CC0-1.0-Universal license, whichever is more permissive.