PEP 818 – Python에 Pyodide 외부 함수 인터페이스의 핵심 추가
- Author:
- Hood Chatham <roberthoodchatham at gmail.com>
- Sponsor:
- Łukasz Langa <lukasz at python.org>
- Discussions-To:
- Discourse thread
- Status:
- Draft
- Type:
- Standards Track
- Created:
- 10-Dec-2025
- Python-Version:
- 3.15
Table of Contents
- 초록
- 동기
- 근거
- 사양
- 이 문서의 의사 코드
- Python과 JavaScript 간 값 변환
- 오류 처리
- 호출 규칙
run_jsmakePythonFunction- JSProxy
JSProxy만들기JSProxy메타클래스JSProxy베이스 클래스- 설정할 플래그 결정
HAS_GET믹스인HAS_SET믹스인HAS_HAS믹스인HAS_INCLUDES믹스인HAS_LENGTH믹스인HAS_DISPOSE믹스인IS_ARRAY믹스인IS_ARRAY_LIKE믹스인IS_CALLABLE믹스인IS_ERROR믹스인IS_ITERABLE믹스인IS_ITERATOR믹스인IS_GENERATOR믹스인IS_MAPPING믹스인IS_MUTABLE_MAPPING믹스인IS_PY_JSON_SEQUENCE믹스인IS_PY_JSON_DICT믹스인IS_DOUBLE_PROXY믹스인
- PyProxy
- 심층 변환
jstypes.global_this모듈jstypes패키지json모듈의 변경 사항
- 하위 호환성
- 보안 관련 사항
- 이 내용을 가르치는 방법
- 참조 구현
- 감사의 말
- Copyright
번역·라이선스 안내
이 비공식 한국어 번역은 원문 Copyright 절의 Public Domain or CC0-1.0, whichever is more permissive 조건에 따라 제공합니다. 원저자와 공식 원문은 그대로 표시합니다. 수정되지 않은 기준 원문 · 공식 최신판
초록
Pyodide는 브라우저를 포함한 자바스크립트 런타임용 Python 배포판입니다. 브라우저는 범용 컴퓨팅 플랫폼입니다. Unix 계열 운영 체제에서 C가 그러한 것처럼, 브라우저 플랫폼에서는 모든 기본 기능이 자바스크립트 언어를 통해 노출됩니다. Pyodide는 수년간 포괄적인 자바스크립트 외부 함수 인터페이스를 포함해 왔습니다. 이는 자바스크립트 세계에서 os 모듈에 해당하는 기능을 제공합니다.
이 PEP는 Pyodide 외부 함수 인터페이스의 핵심을 Python에 추가할 것을 제안합니다.
동기
Pyodide 프로젝트는 자바스크립트 런타임용 Python 배포판입니다. Pyodide는 매우 인기 있는 프로젝트입니다. 2025년에 Pyodide는 JsDelivr에서 10억 건이 넘는 요청을 받았습니다. 인기는 빠르게 증가하고 있습니다. 지난 2년 동안 각각 사용량이 두 배 이상 증가했습니다.
Pyodide에는 여러 구성 요소가 포함되어 있습니다.
- Emscripten 컴파일러 툴체인으로 이식한 CPython입니다(리눅스 C/C++ 프로그램을 자바스크립트와 WebAssembly로 컴파일하는 툴체인입니다).
- 자바스크립트에서 Python을 호출하고 Python에서 자바스크립트를 호출하기 위한 외부 함수 인터페이스입니다.
- Python 런타임과 패키지 설치를 관리하기 위한 자바스크립트 프로그래밍 인터페이스입니다.
- 네이티브 확장을 위한 ABI입니다.
- Pyodide에서 사용할 수 있도록 해당 ABI와 호환되는 Python 패키지를 크로스 컴파일하는 툴체인입니다.
장기적으로는 Pyodide 프로젝트의 런타임 구성 요소 (1)–(4)를 CPython에 상류 반영하고자 합니다. 2022년에 Christian Heimes가 CPython의 Emscripten 이식판인 (1)을 상류 반영했으며, Emscripten은 현재 3등급 지원 플랫폼입니다(PEP 776 참조). PEP 783은 Pyodide 호환 휠을 PyPI에 업로드할 수 있도록 허용할 것을 제안합니다. 이것들이 Emscripten-CPython 호환 휠이 되기 위해 필요한 작업은 (2) Python/자바스크립트 외부 함수 인터페이스와 (4) 네이티브 확장을 위한 ABI를 상류 반영하는 것입니다. 이 PEP는 (2) Python/자바스크립트 외부 함수 인터페이스를 부분적으로 상류 반영하는 것을 다룹니다.
이 인터페이스는 리눅스용 Python의 os 모듈과 유사합니다. 모든 I/O는 libc를 거쳐야 하며, os 모듈은 Python 코드에서 libc 호출에 접근할 수 있도록 합니다. 마찬가지로 자바스크립트 런타임에서 실제 작업을 수행하려면 자바스크립트를 호출해야 합니다. 예를 들어 화면에 콘텐츠를 표시하고, 사용자 입력을 받고, 이벤트를 처리하고, 데이터베이스에 접근하려면 자바스크립트 호출이 필요합니다. 예를 들어 Python이 자바스크립트 외부 함수 인터페이스를 갖추면 Emscripten에서 urllib를 지원할 수 있게 됩니다. 이후 단계에서 urllib3, aiohttp 및 httpx를 지원하려면 외부 함수 인터페이스가 필요합니다.
이 PEP의 분량을 적절하게 유지하기 위해 외부 함수 인터페이스의 “핵심”에 초점을 맞춥니다. 세 가지 영역은 향후 PEP에 남겨 둡니다.
- asyncio
- 버퍼 프로토콜과 자바스크립트 대응 요소 간의 통합
- Python 런타임을 관리하기 위한 자바스크립트 인터페이스
근거
여기서 우리의 목표는 Pyodide의 대규모 기존 사용자 집단에 대해 필요한 수준을 넘어 하위 호환성을 깨뜨리지 않으면서 Pyodide의 외부 함수 인터페이스를 메인라인에 반영하는 것입니다. 반면, 호환성을 깨뜨리는 변경을 하기에는 지금이 가장 좋은 시기입니다.
이를 염두에 두고, 여기서는 우리의 설계가 완벽하다는 점이 아니라 어떠한 변경의 비용이 이점보다 크다는 점을 정당화하고자 합니다.
객체 변환
가장 근본적인 결정은 한 언어에서 다른 언어로 객체를 어떻게 변환할지에 관한 것입니다. 객체를 변환할 때는 값을 대상 언어의 유사한 객체로 변환하거나, 원래 객체를 “감싸는” 프록시를 만들 수 있습니다. 여기에는 몇 가지 고려 사항이 적용됩니다.
- 변경 가능성: 인자를 변경할 것으로 예상되는 함수를 호출하는 경우에는 인자를 변환하지 않고 프록시로 처리하는 것이 중요합니다. 그렇지 않으면 함수가 복사본을 변경하고, 우리는 그 복사본을 버리게 됩니다. 따라서 암시적 변환은 변경 불가능한 객체에 대해서만 합리적인 선택입니다.
- 왕복 동작: Python에서 JavaScript를 거쳐 다시 Python으로 객체를 전달했을 때 원래 Python 객체가 반환되고, 그 반대의 경우도 성립하는 것이 매우 바람직합니다. 객체가 변경 불가능하다면 결과가 원래 객체와 동일한 객체가 아니라 단지 같은 값이어도 괜찮습니다. 객체가 변경 가능하다면 동일한 객체여야 합니다.
- 성능 특성: 복잡한 객체를 변환하려면 많은 사전 작업이 필요합니다. 객체가 최소한으로만 사용된다면 성능이 떨어질 수 있습니다. 반면 프록시를 통한 객체에 대한 각 접근은 네이티브 객체에 대한 접근보다 느리므로, 객체를 많이 사용하는 경우에는 먼저 변환하는 것이 프록시를 사용하는 것보다 효율적입니다. 기본적으로 프록시를 사용하고 사용자가 원할 때 명시적으로 변환할 수 있도록 하면 성능을 최대한 제어할 수 있습니다.
- 사용 편의성: 네이티브 객체는 많은 경우 다루기가 더 쉽습니다.
JavaScript에는 다음과 같은 변경 불가능한 타입이 있습니다: string, undefined, boolean, number 및 bigint. 또한 특수 값인 null도 있습니다.
이 중 string과 boolean은 각각 str및 bool에 직접 대응합니다. Number.isSafeInteger()가 true를 반환하면 number를 int로 변환하고, 그렇지 않으면 float로 변환합니다. 반대로 float는 number로 변환하고, int는 2**53을 초과하지 않는 한 number로 변환하며, 초과하는 경우에는 bigint로 변환합니다. bigint의 변환으로 사용하기 위해 JSBigInt이라는 int의 새 하위 클래스를 만듭니다. undefined는 누락된 인자의 기본값이므로 None에 해당합니다. null의 변환으로 사용하기 위해 JSNull타입의 새로운 거짓 같은 단일 Python 값인 jsnull을 고안합니다. 그 밖의 모든 타입은 프록시로 처리합니다.
특히 tuples는 변경 불가능하지만 JavaScript에는 이에 상응하는 것이 없으므로 프록시로 처리합니다. 필요한 경우 toJs()메서드를 사용하여 수동으로 Array로 변환할 수 있습니다.
프록시
JSProxy는 JavaScript 객체에 접근하는 데 사용되는 Python 객체입니다. JSProxy가 존재하는 동안에는 기반 JavaScript 객체가 테이블에 유지되므로 가비지 컬렉션되지 않습니다.
PyProxy는 Python 객체에 접근하는 데 사용되는 JavaScript 객체입니다. PyProxy가 생성되면 기반 Python 객체의 참조 카운트가 증가합니다. .destroy()메서드가 호출되면 기반 Python 객체의 참조 카운트가 감소하고 프록시가 비활성화됩니다. 이후 이를 사용하려고 시도하면 오류가 발생합니다.
기본 JSProxy는 속성 접근, 동등성 검사, __repr__, __eq__, __bool__ 및 몇 가지 다른 편의 메서드를 구현합니다. 또한 추상 Python 객체 프로토콜을 추상 JavaScript 객체 프로토콜에 매핑하고 그 반대 방향으로도 매핑하여 많은 믹스인을 정의합니다. 이 PEP에서 설명하는 매핑은 다음과 같습니다.
기본 프록시(모든 객체에 공통된 속성):
__getattribute__<==>Reflect.get(프록시 핸들러)__setattr__<==>Reflect.set(프록시 핸들러)__eq__<==>===(객체 동일성)__repr__<==>toString
__str__구현에서는 __repr__를 사용하는 기본 구현을 상속합니다.
다음과 같은 프로토콜 간 매핑을 믹스인으로 구현합니다. 프록시를 생성할 때 이러한 추상 및 구체 프로토콜 중 지원하는 것을 기능 감지하고, 적절한 믹스인을 포함한 프록시 클래스를 생성합니다.
__iter__<==>[Symbol.iterator]__next__<==>next__len__<==>length,size__getitem__<==>get__setitem__,__delitem__<==>set,delete__contains__<==>includes,has__call__<==>Reflect.apply(프록시 핸들러)Generator<==>GeneratorException<==>ErrorMutableSequence<==>Array
JavaScript 객체에 [Symbol.dispose]()메서드가 있으면 Python 객체를 컨텍스트 관리자로 만들지만, 현재는 [Symbol.dispose]()를 구현하는 데 컨텍스트 관리자를 사용하지 않습니다.
JavaScript에는 Reflect.construct (new 키워드)도 있습니다. 호출 가능 객체인 JSProxy에는 Reflect.construct에 대응하는 new()라는 메서드가 있습니다.
다음과 같은 추가 매핑이 Pyodide에 정의되어 있습니다. 장차 이러한 매핑을 Python 자체에 추가할 예정이지만, 향후 PEP로 미루어 두었습니다.
__await__<==>then__aiter__<==>[Symbol.asyncIterator]__anext__<==>next(__next__와 동일하며, 구분하려면[Symbol.asyncIterator]의 존재 여부를 확인합니다)AsyncGenerator<==>AsyncGenerator- 버퍼 프로토콜 <==> 타입 배열
- 비동기 컨텍스트 관리자는
[Symbol.asyncDispose]를 구현하는 JSProxy에 구현됩니다.
프록시의 가비지 컬렉션 및 소멸
우리가 직면하는 가장 근본적인 어려움은 두 개의 가비지 컬렉터, 즉 Python 가비지 컬렉터와 JavaScript 가비지 컬렉터가 존재한다는 점입니다. Python에서 JavaScript를 거쳐 다시 Python으로 이어지는 모든 참조 순환은 누수됩니다. 또한 순환이 없더라도 JavaScript 가비지 컬렉터는 PyProxy가 얼마나 많은 메모리를 소유하는지, Python 가비지 컬렉터가 얼마나 큰 메모리 압박을 받는지 전혀 알 수 없습니다.
이러한 이유로 언어 간 참조를 수동으로 끊을 방법을 포함해야 합니다. Python에서는 객체의 참조 횟수가 0에 도달하면 소멸자가 즉시 실행됩니다. 따라서 프로그래머가 JavaScript 객체를 수동으로 해제하려는 경우 해당 객체에 대한 모든 참조를 삭제하면 되고, 그러면 JavaScript 가비지 컬렉터가 이를 회수할 수 있습니다.
반면 JavaScript 파이널라이저는 신뢰할 수 없습니다. 이를 언어에 도입한 제안에서는 다음과 같이 설명합니다.
애플리케이션이나 라이브러리가 GC가 [파이널라이저를 호출하는] 시기적절하고 예측 가능한 방식에 의존한다면, 실망할 가능성이 높습니다. 정리가 예상보다 훨씬 늦게 발생하거나 전혀 발생하지 않을 수 있습니다.…
[파이널라이저]는 외부 리소스를 정리하는 일반적인 방법으로 사용하기보다는 과도한 메모리 사용을 방지하거나 특정 버그에 대비하는 보완책으로 사용하는 것이 가장 좋습니다.
https://github.com/tc39/proposal-weakrefs?tab=readme-ov-file#a-note-of-caution
PyProxy에는 PyProxy를 수동으로 분리하고 Python 참조를 해제하는 destroy() 메서드가 있습니다. PyProxy를 파괴하는 것이 이를 정리하는 올바르고 정상적인 방법이라고 생각합니다. 제안에서 권장하는 대로 파이널라이저는 보완책으로 취급합니다. Pyodide 테스트 모음에서는 대부분의 테스트에서 모든 PyProxy를 수동으로 파괴하도록 요구합니다. 이는 API가 이러한 사용 편의성을 유지하는 방식으로 설계되었는지 확인하는 데 도움이 됩니다.
호출 규약
JavaScript에서 Python 함수 호출하기
호출 가능 객체인 PyProxy를 호출하려면 다음 단계를 수행합니다.
- 각 인자를 JavaScript에서 Python으로 변환하고 인자를 C 배열에 배치합니다.
PyObject_VectorCall을 사용하여 Python 객체를 호출합니다.- JavaScript 오류가 발생하면 이는 치명적입니다. Python 인터프리터 불변 조건이 위반된 것입니다. 치명적 오류를 보고하고 Python 인터프리터를 종료합니다.
- Python 오류 플래그가 설정되어 있으면
sys.last_value를 현재 예외로 설정합니다. Python 예외를 JavaScriptPythonError객체로 변환합니다. 이PythonError객체에는 유형, Python 예외의 형식화된 트레이스백, 원래 Python 예외에 대한 약한 참조가 기록됩니다. 이PythonError를 던집니다. - Python에서 JavaScript로 결과를 변환하여 반환합니다.
여기서 JSProxy가 생성되었지만 Python 함수가 이에 대한 참조를 저장하지 않으면 즉시 해제된다는 점에 유의하십시오. JavaScript 오류는 자주 누출되고 Python 오류 객체는 상당한 양의 메모리를 차지할 수 있는 프레임 객체에 대한 참조를 보유하므로, JavaScript 오류는 Python 예외에 대한 강한 참조를 보유하지 않습니다.
Python에서 JavaScript 함수 호출하기
호출 가능 객체인 JSProxy를 호출하려면 다음 단계를 수행합니다.
pyproxies라는 빈 배열을 만듭니다.- 각 위치 인자를 Python에서 JavaScript로 변환하고, 이 인자들을
jsargs라는 JavaScript 배열에 배치합니다. 이 방식으로PyProxy가 생성되면 해당 객체에 JavaScript 종료자를 등록하지 말고pyproxies에 추가합니다. - 키워드 인자가 있으면 빈 JavaScript 객체
jskwargs를 생성하고, 각 키워드 인자를 JavaScript로 변환한 다음jskwargs[key] = jskwarg를 할당합니다.jskwargs를jsargs에 추가합니다. 이 방식으로PyProxy가 생성되면 해당 객체에 JavaScript 종료자를 등록하지 말고pyproxies에 추가합니다. - JavaScript 함수를 호출하고 결과를
jsresult에 저장합니다. - 오류가 발생하면 다음과 같이 처리합니다.
- 오류가
PythonError이고 Python 예외에 대한 약한 참조가 아직 유효하면, 참조된 Python 예외를 발생시킵니다. - 그렇지 않으면 JavaScript의 예외를 Python으로 변환하고 그
결과를 발생시킵니다.
JSException객체가 원래 JavaScript 오류에 대한 참조를 보유한다는 점에 유의하십시오.
- 오류가
jsresult가 JavaScript 제너레이터이면pyproxies를 순회하면서 각 항목에 JavaScript 종료자를 등록합니다. 제너레이터가 소진될 때pyproxies를 삭제하는 새 제너레이터로 기존 제너레이터를 감쌉니다. 감싼 제너레이터를 Python으로 변환하여 반환합니다.- 그렇지 않으면
jsresult를 Python으로 변환하고 이를pyresult에 저장합니다. pyproxies를 순회하면서 해당 항목들을 삭제합니다.jsresult가PyProxy이면 그것도 삭제합니다.pyresult를 반환합니다.
이는 C Python API의 호출 규약을 모델로 한 것입니다.
JSProxy 호출 규약의 옹호
JavaScript에서 Python으로 호출하는 규약은 논란의 여지가 없으므로 이를 옹호하지 않겠습니다. Python에서 JavaScript로 호출하는 규약은 더 논란의 여지가 있으므로, 이 방식이 대안보다 더 나은 설계라고 생각하는 이유를 여기서 설명하겠습니다.
이 설계의 가장 큰 단점은 호출 대상이 인자를 유지하려는 경우 사용 편의성이 그다지 높지 않다는 점입니다. 그러나 이러한 단점보다 이점이 더 크다고 주장합니다.
이 접근 방식의 가장 큰 장점은 Python의 존재를 인식하지 못하는 JavaScript 함수를 메모리 누수 없이 사용할 수 있게 한다는 점입니다. 또 다른 장점은 PyProxy에 대한 파이널라이저 등록이 어느 정도 비용이 들기 때문에, 이 단계를 생략하면 특정 Python에서 JavaScript로의 호출에 대한 오버헤드를 크게 줄일 수 있다는 점입니다.
호출 규약의 단점 예시
Python에서 JavaScript로 호출하는 규약에 대한 흔한 불만을 보여 주는 것부터 시작하겠습니다. 다음 예를 살펴보십시오.
from jstypes.code import run_js
set_x = run_js("(x) => { globalThis.x = x; }")
get_x = run_js("(x) => globalThis.x")
set_x({})
get_x()
이 코드는 잘못되었습니다. set_x를 호출하면 PyProxy가 생성되지만 호출이 완료되면 소멸됩니다. get_x()를 호출하면 다음 오류가 발생합니다.:
This borrowed proxy was automatically destroyed at the end of a function call.
이를 수정하여 메모리를 올바르게 관리하려면 set_x를 다음 함수로 변경할 수 있습니다.
(x) => {
globalThis.x?.destroy?.();
globalThis.x = x?.copy?.() ?? x;
}
또는 다음과 같이 create_proxy()를 사용하여 Python에서 메모리를 관리할 수 있습니다.
from jstypes.ffi import JSDoubleProxy
from jstypes.code import run_js
setXJs = run_js("(x) => { globalThis.x = x; }")
def set_x(x):
orig_x = get_x()
if isinstance(orig_x, JSDoubleProxy):
orig_x.destroy()
xpx = create_proxy(x)
setXJs(xpx)
이 추가 보일러플레이트를 올바르게 작성하는 것은 그다지 어렵지 않으며, C에서 속성을 할당하는 데 필요한 작업과 대략 동일합니다. 그러나 이는 사용자에게 상당한 복잡성 비용을 부과하므로, 이것이 대안보다 나은 이유를 정당화해야 합니다.
이 호출 규약으로 단순해지는 사용 사례
버퍼를 반환하는 Python 함수 render()와 캔버스에 버퍼를 표시하는 JavaScript 함수 drawImageToCanvas(buffer)가 있다고 가정하십시오. 버퍼가 4개의 색상 채널을 가진 1024 × 1024 비트맵이라면 4메가바이트 버퍼입니다. 다음 코드를 상상해 보십시오.
@create_proxy
def main_loop():
update()
buf = render()
drawImageToCanvas(buffer)
requestAnimationFrame(main_loop)
여기에서 설명한 호출 규약을 사용하면 각 호출 후 버퍼가 정상적으로 해제되고 메모리 사용량이 일정하게 유지되며, 제 테스트에서는 57메가바이트로 유지됩니다.
buffer를 해제하기 위해 JavaScript 파이널라이저에 의존하면, 제 테스트에서는 malloc이 WebAssembly 힙의 공간을 모두 사용하고 추가 메모리를 요청할 때까지 JavaScript 파이널라이저가 실행되지 않으며, 그 결과 수 분에 걸쳐 WebAssembly 힙이 허용된 최대 크기인 4기가바이트까지 점진적으로 증가한 후 메모리 오류가 발생합니다.
이제 협력적인 drawImageToCanvas()구현이라면 작업이 끝났을 때 buffer를 소멸시킬 수 있지만, 호출 규약을 설계할 때 제 철학은 Python에서 메모리 관리를 처리할 수 있어야 한다는 것이었습니다. 따라서 현재 접근 방식과 같은 것이 필요합니다.
새로운 최상위 패키지
jstypes라는 새로운 최상위 패키지를 도입합니다.
jstypes패키지에는 jstypes.code와 jstypes.ffi라는 두 모듈이 있습니다. jstypes.global_this패키지는 JavaScript 전역 범위인 globalThis입니다. jstypes.global_this모듈에 어떤 값 집합이 존재하는지는 JavaScript 런타임과 Python 런타임이 주 스레드에 있는지 작업자 스레드에 있는지에 따라 달라집니다. 예를 들어 from jstypes.global_this import Buffer는 Node에서는 성공하지만 브라우저에서는 실패합니다.
jstypes.code는 run_js함수를 노출합니다.
jstypes.ffi는 다음 함수를 노출합니다.
create_proxy- Python에서
PyProxy를 생성합니다. Python에서PyProxy의 수명을 제어하는 데 사용됩니다. jsnull- JavaScript
null값과 상호 변환되는 특수 값입니다. JSNulljsnull의 타입입니다.JSBigIntint의 서브타입으로, JavaScriptbigint와 상호 변환됩니다.to_js- Python 값을 JavaScript로 심층 변환합니다.
또한 JSProxy와 그 서브타입도 포함합니다:
JSProxy- 이는
type(run_js("({})"))입니다. JSArray- 이는
type(run_js("[]"))입니다. JSCallable- 이는
type(run_js("() => {}"))입니다. JSDoubleProxy- 이는
type(create_proxy({}))입니다. JSException- 이는
type(run_js("new Error()"))입니다. JSGenerator- 이는
type(run_js("(function*(){})()"))입니다. JSIterable- 이는
type(run_js("({[Symbol.iterator](){}})"))입니다. JSIterator- 이는
type(run_js("({next(){}}))"))입니다. JSMap- 이는
type(run_js("({get(){}})"))입니다. JSMutableMap- 이는
type(run_js("new Map()"))입니다.
사양
이 문서의 의사 코드
이 PEP의 의사 코드는 일반적으로 Python 또는 JavaScript로 작성합니다. 특히 흥미롭다고 생각하는 경우를 제외하고 리소스 관리와 예외 처리는 대부분 생략합니다. 오류가 발생하면 모든 리소스를 암묵적으로 정리하고 오류를 전파합니다. 실제 코드의 상당 부분은 리소스 관리와 예외 처리로 구성됩니다.
대부분의 경우 코드는 작성된 그대로 작동하지만, 일부 지점에서는 Python에서 C API를 직접 호출하거나, 실행되지는 않지만 의도가 명확하다고 생각하는 코드를 다른 방식으로 작성합니다.
Python 코드에서 JavaScript 함수를 인라인으로 실행하려는 경우 다음과 같이 작성합니다:
jsfunc = run_js("(x, y) => doSomething")
jsfunc(x, y)
반대로 JavaScript에서 Python 코드를 인라인으로 실행하려는 경우 다음과 같이 작성합니다:
const pyfunc = makePythonFunction(`
def pyfunc(x, y):
# do something
`);
pyfunc(x, y)
대부분의 경우 성능이 문제가 되지 않는다면 이 코드를 실제로 사용할 수 있습니다. 일부 위치에서는 부트스트래핑 문제가 발생할 수 있습니다.
첫 번째 작업은 Python 호출 가능 객체 run_js와 JavaScript 호출 가능 객체 makePythonFunction를 정의하는 것입니다. run_js는 JSProxy이고 makePythonFunction은 PyProxy입니다.
이를 이해하려면 다음을 설명해야 합니다.
- JavaScript에서 Python으로, 그리고 Python에서 JavaScript로 값을 변환하는 방법
- JavaScript에서 Python 함수를 호출하는 방법과 Python에서 JavaScript 함수를 호출하는 방법
JavaScript에서 PyObject*를 number로 직접 나타낼 수 있으므로, JavaScript에서 PyObject*를 호출하는 과정을 설명할 수 있습니다. 반면 JavaScript 객체는 Python에서 직접 나타낼 수 없으므로, 해당 객체의 JSProxy를 생성해야 합니다. 먼저 JSProxy를 호출하는 과정을 설명하며, 이를 생성하는 과정은 JSProxies 절에서 설명합니다.
Python과 JavaScript 간 값 변환
일부 원시 타입은 Python과 JavaScript 간에 암묵적으로 변환됩니다. 암묵적 변환은 왕복 변환이 되도록 설계되므로, Python에서 JavaScript로 변환한 후 다시 Python으로 변환하거나 JavaScript에서 Python으로 변환한 후 다시 JavaScript로 변환할 때 결과는 시작할 때의 원시 타입과 동일합니다. 한 가지 예외는 2^53보다 작은 JavaScript BigInt가 왕복 변환 시 Number로 변환된다는 것입니다. undefined를 None으로 변환하고, null을 변환하기 위해 특수한 거짓값 싱글턴 jstypes.ffi.jsnull을 도입합니다. jstypes.ffi.JSBigInt이라고 하는 int의 서브타입도 도입하며, 이는 JavaScript bigint와 상호 변환됩니다.
암묵적 변환은 C 함수 _Py_python2js와 _Py_js2python()를 사용하여 수행합니다. JSVal타입은 Python에서 나타낼 수 없으므로 이러한 함수는 Python 코드에서 직접 호출할 수 없습니다.
Python에서 JavaScript로의 암묵적 변환
JSVal _Py_python2js_track_proxies(PyObject* pyvalue, JSVal pyproxies, bool gc_register)는 Python에서 JavaScript로의 암묵적 변환을 담당합니다. 다음 단계를 수행합니다.
pyvalue가None이면undefined를 반환합니다.pyvalue가jsnull이면null을 반환합니다.pyvalue가True이면true를 반환합니다.pyvalue가False이면false를 반환합니다.pyvalue가str인 경우 문자열을 JavaScript로 변환하고 결과를 반환합니다.pyvalue가JSBigInt의 인스턴스인 경우BigInt로 변환합니다.pyvalue가int이고2^53보다 작은 경우Number로 변환합니다. 그렇지 않으면BigInt로 변환합니다.pyvalue가float인 경우Number로 변환합니다.pyvalue가JSProxy인 경우 래핑된 JavaScript 값으로 변환합니다.result를createPyProxy(pyvalue, {gcRegister: gc_register})로 정의합니다.pyproxies가 배열인 경우result를pyproxies에 추가합니다.
JSVal _Py_python2js(PyObject* pyvalue)를 _Py_python2js_track_proxies(pyvalue, Js_undefined, true)로 정의합니다.
JavaScript에서 Python으로의 암시적 변환
PyObject* _Py_js2python(JSVal jsvalue)는 JavaScript에서 Python으로의 암시적 변환을 담당합니다.
먼저 도우미 함수 PyObject* _Py_js2python_immutable(JSVal jsvalue)를 정의하며 다음 단계를 수행합니다.
jsvalue가undefined인 경우None을 반환합니다.jsvalue가null인 경우jsnull을 반환합니다.jsvalue가true인 경우True를 반환합니다.jsvalue가false인 경우False를 반환합니다.jsvalue가string인 경우 문자열을 Python으로 변환하고 결과를 반환합니다.jsvalue가Number이고Number.isSafeInteger(jsvalue)가true를 반환하는 경우jsvalue를int로 변환합니다. 그렇지 않으면float로 변환합니다.jsvalue가BigInt인 경우JSBigInt로 변환합니다.jsvalue가 파괴되지 않은PyProxy라면, 래핑된 Python 값으로 변환하십시오.jsvalue가 파괴된PyProxy인 경우 이를 나타내는 오류를 발생시킵니다.NoValue를 반환합니다.
_Py_js2python(JSVal jsvalue)는 다음 단계를 수행합니다.
result를_Py_js2python_immutable(jsvalue)로 정의합니다.result가NoValue가 아닌 경우result를 반환합니다.create_jsproxy(jsvalue)를 반환합니다.
오류 처리
JavaScript와 C 사이의 경계에서는 오류를 변환해야 합니다.
C에서 JavaScript 코드 실행
C에서 JavaScript 코드를 실행할 때마다 이를 try/catch 블록으로 감쌉니다. 오류가 포착되면 _Py_js2python(jserror)를 사용하여 이를 Python 예외로 변환하고, Python 오류 플래그를 이 Python 예외로 설정한 다음, 오류를 알리는 데 적절한 오류 값을 반환합니다. 이를 통해 C에서 호출할 수 있고 C API에 대한 CPython의 일반적인 규칙을 따르는 JavaScript 함수를 편리하게 생성할 수 있습니다.
JavaScript에서 C 코드 실행
JavaScript에서 C를 호출할 때마다 다음 상용구로 호출을 감쌉니다.
try {
result = some_c_function();
} catch (e) {
// If an error was thrown here, the C runtime state is corrupted.
// Signal a fatal error and tear down the interpreter.
fatal_error(e);
}
// Depending on the API, we check for -1, 0, _PyErr_Occurred(), etc to
// decide if an error occurred.
if (result === -1) {
// This function takes the error flag and converts it to a JavaScript
// exception. It leaves the error flag cleared.
throw __Py_pythonexc2js();
}
호출 규칙
JavaScript에서 Python 함수 호출
JavaScript에서 PyObject*를 호출하려면 다음 코드를 사용합니다.
function callPyObjectKwargs(pyfuncptr, jsargs, kwargs) {
const num_pos_args = jsargs.length;
const kwargs_names = Object.keys(kwargs);
const kwargs_values = Object.values(kwargs);
const num_kwargs = kwargs_names.length;
jsargs.push(...kwargs_values);
// apply the usual error handling logic for calling from JavaScript into C.
return _PyProxy_apply(pyfuncptr, jsargs, num_pos_args, kwargs_names, num_kwargs);
}
_PyProxy_apply(PyObject* 호출 가능 객체, JSVal jsargs, Py_ssize_t num_pos_args, JSVal kwargs_names, Py_ssize_t num_kwargs)
total_args를num_pos_args + numkwargs로 정의합니다.- 길이가
total_args인 C 배열pyargs를 생성합니다. i를0부터total_args - 1까지 변화시키면서 다음을 수행합니다:- JavaScript 코드
jsargs[i]를 실행하고 결과를jsitem에 저장합니다. pyargs[i]를_Py_js2python(jsitem)로 설정합니다.
- JavaScript 코드
- 길이가
numkwargs인 새 튜플을pykwnames로 정의합니다. i를0부터numkwargs - 1까지 변화시키면서 다음을 수행합니다:- JavaScript 코드
jskwnames[i]를 실행하고 결과를jskey에 저장합니다. pykwnames의 i번째 항목을_Py_js2python(jsitem)로 설정합니다.
- JavaScript 코드
pyresult를PyObject_Vectorcall(callable, pyargs, num_pos_args, pykwnames)로 정의합니다._Py_python2js(pyresult)를 반환합니다.
Python에서 JavaScript 함수 호출
``JSMethod_ConvertArgs(posargs, kwargs, pyproxies)``
먼저 Python 인자를 JavaScript 인자 배열로 변환하는 함수 JSMethod_ConvertArgs를 정의합니다. 이 단계에서 생성된 PyProxy는 종료 레지스트리에서 추적되지 않으며 JavaScript 목록 pyproxies에 추가되므로, 나중에 이를 소멸시키거나 추적할 수 있습니다. 이 함수는 다음 단계를 수행합니다:
- 새 빈 JavaScript 목록을
jsargs로 정의합니다. - 각 위치 인자에 대해 다음을 수행합니다:
JSVal jsarg = _Py_python2js_track_proxies(pyarg, proxies, /*gc_register:*/false);로 설정합니다._PyJsvArray_Push(jsargs, arg);를 호출합니다.
- 키워드 인자가 있는 경우:
jskwargs를 새로운 빈 JavaScript 객체로 설정합니다.- 각 키워드 인자 pykey, pyvalue에 대해 다음을 수행합니다:
JSVal jskey = _Py_python2js(pykey)로 설정합니다. ii.JSVal jsvalue = _Py_python2js_track_proxies(pyvalue, proxies, /*gc_register:*/false)로 설정합니다. iii.jskwargs의jskey속성을jsvalue로 설정합니다.
_PyJsvArray_Push(jsargs, jskwargs);를 호출합니다.
jsargs를 반환합니다.
``JSMethod_Vectorcall(jsproxy, posargs, kwargs)``
함수의 각 JSProxy에는 기저 JavaScript 함수와 기저 this값이 있습니다.
jsproxy와 연결된 JavaScript 함수를jsfunc라고 합니다.jsproxy와 연결된this값을jsthis라고 합니다.pyproxies를 새로운 빈 JavaScript 리스트라고 합니다.JSMethod_ConvertArgs(posargs, kwargs, pyproxies)를 실행하고 그 결과를jsargs에 저장합니다.- JavaScript 코드
Function.prototype.apply.apply(jsfunc, [ jsthis, jsargs ])를 실행하고 그 결과를jsresult에 저장합니다. (C에서 JavaScript를 호출할 때 일반적인 오류 처리를 적용합니다.) jsresult가PyProxy인 경우 JavaScript 코드pyproxies.push(jsresult)를 실행합니다.destroy_args를true로 설정합니다.jsresult가Generator인 경우destroy_args를false로 설정하고jsresult를wrap_generator(jsresult, pyproxies)로 설정합니다._Py_js2python(jsresult)를 실행하고 그 결과를pyresult에 저장합니다.destroy_args가true인 경우pyproxies의 모든 프록시를 제거합니다.destroy_args가false인 경우pyproxies의 모든 프록시를 GC에 등록합니다.pyresult를 반환합니다.
wrap_generator(jsresult, pyproxies)는 JavaScript 제너레이터를 새로운 제너레이터로 감싸고, 제너레이터가 소진될 때 pyproxies의 모든 프록시를 제거하는 JavaScript 함수입니다.
run_js
Python 객체 jstypes.code.run_js는 다음과 같이 정의됩니다:
- JavaScript 코드
eval을 실행하고 결과를jseval에 저장합니다. _Py_js2python(jseval)을 실행하고 결과를run_js에 저장합니다.
makePythonFunction
run_js와 달리 JavaScript 객체 makePythonFunction은 순전히 의사 코드 작성을 위한 것이며 API의 일부로 포함되지 않습니다. 다음과 같이 makePythonFunction을 정의합니다.
def make_python_function(code):
mod = ast.parse(code)
if isinstance(mod.body[0], ast.FunctionDef):
d = {}
exec(code, d)
return d[mod.body[0].name]
return eval(code)
- 위의 함수를
make_python_function으로 지정합니다. _Py_python2js(make_python_function)을 실행하고 결과를makePythonFunction에 저장합니다.
JSProxy
JavaScript 객체가 지원할 수 있는 14개의 서로 다른 추상 프로토콜을 정의합니다. 이들은 각각 JSProxy의 타입 플래그에 해당합니다. 또한 IS_PY_JSON_DICT 및 IS_PY_JSON_SEQUENCE라는 두 개의 추가 플래그가 있으며, 이는 JSProxy.as_py_json() 메서드에서 설정되고 기반 JavaScript 객체의 속성을 반영하지 않습니다.
HAS_GET- JavaScript 객체에
get()메서드가 있는지 여부를 나타냅니다. 존재하는 경우JSProxy에서__getitem__을 구현하는 데 사용합니다. HAS_HAS- JavaScript 객체에
has()메서드가 있는지 여부를 나타냅니다. 존재하는 경우JSProxy에서__contains__를 구현하는 데 사용합니다. HAS_INCLUDES- JavaScript 객체에
includes()메서드가 있는지 여부를 나타냅니다. 존재하는 경우JSProxy에서__contains__를 구현하는 데 사용합니다. 둘 다 존재하는 경우includes()보다has()를 사용하는 것을 선호합니다. HAS_LENGTH- JavaScript 객체에
length또는size속성이 있는지 여부를 나타냅니다.JSProxy에서__len__을 구현하는 데 사용합니다. HAS_SET- JavaScript 객체에
set()메서드가 있는지 여부를 나타냅니다. 존재하는 경우JSProxy에서__setitem__을 구현하는 데 사용합니다. HAS_DISPOSE- JavaScript 객체에
[Symbol.dispose]()메서드가 있는지 여부를 나타냅니다. 존재하는 경우__enter__와__exit__를 구현하는 데 사용합니다. IS_ARRAY- JavaScript 객체에
Array.isArray()를 적용한 결과가true인지를 나타냅니다. 설정되어 있으면JSProxy는collections.abc.MutableSequence의 인스턴스가 됩니다. IS_ARRAY_LIKEArray.isArray()가false를 반환하고 객체에length속성과IS_ITERABLE이 있는 경우 이를 설정합니다. 설정되어 있으면JSProxy는collections.abc.Sequence의 인스턴스가 됩니다. 다음과 같이 webidl에 정의된 여러 인터페이스가 이에 해당합니다: NodeListIS_CALLABLE- JavaScript 객체의
typeof가"function"인지를 나타냅니다. 설정되어 있으면JSProxy에__call__을 구현하는 데 사용됩니다. IS_ERROR- JavaScript 객체가
Error인지를 나타냅니다. 그렇다면JSProxy가Exception을 상속하여 발생시킬 수 있도록 합니다. IS_GENERATOR- JavaScript 객체가 제너레이터인지를 나타냅니다. 그렇다면
JSProxy는collections.abc.Generator의 인스턴스가 됩니다. IS_ITERABLE- JavaScript 객체에
[Symbol.iterator]메서드가 있거나IS_PY_JSON_DICT플래그가 설정되어 있는지를 나타냅니다. 그렇다면 이를 사용하여JSProxy에__iter__를 구현합니다. IS_ITERATOR- JavaScript 객체에
next()메서드가 있고[Symbol.asyncIterator]메서드는 없는지를 나타냅니다. 그렇다면 이를 사용하여JSProxy에__next__를 구현합니다. ([Symbol.asyncIterator]메서드가 있으면next()메서드를 사용하여__anext__를 구현해야 한다고 가정합니다.) IS_PY_JSON_DICTArray가 아닌 경우as_py_json()메서드가JSProxy에 설정합니다. 이것이 설정되면JSProxy의__getitem__이 JavaScript 객체에 대한 속성 접근으로 변환됩니다. 또한 프록시를 순회하거나 인덱싱하여 반환되는 값에도 적절하게IS_PY_JSON_DICT또는IS_PY_JSON_SEQUENCE가 설정됩니다.IS_PY_JSON_SEQUENCEArray인 경우as_py_json()메서드가JSProxy에 설정합니다. 이것이 설정되면JSProxy를 인덱싱하거나 순회할 때 결과에as_py_json()를 호출합니다.IS_MAPPINGHAS_GET,HAS_LENGTH,IS_ITERABLE플래그가 설정되어 있거나IS_PY_JSON_DICT가 설정되어 있는 경우 이를 설정합니다. 이 경우JSProxy는collections.abc.Mapping의 인스턴스가 됩니다.IS_MUTABLE_MAPPING- 플래그
IS_MAPPING및HAS_SET가 설정되어 있거나IS_PY_JSON_DICT가 설정되어 있으면 이를 설정합니다. 이 경우JSProxy는collections.abc.MutableMapping의 인스턴스가 됩니다.
JSProxy 만들기
JavaScript 객체와 값 jsthis를 사용하여 JSProxy를 만들려면 다음 단계를 수행합니다.
- JavaScript 객체에 적합한 타입 플래그를 계산합니다.
- 설정된 타입 플래그 집합에 적합한 믹스인을 포함하는 적절한
JSProxy클래스를 가져오거나 생성하여 캐시합니다. - JavaScript 객체에 대한 참조와
jsthis값을 사용하여 클래스를 인스턴스화합니다.
값 jsthis는 함수를 호출할 때 this의 값을 결정하는 데 사용됩니다. jsobj가 호출 가능 객체가 아니면 아무런 효과가 없습니다.
다음은 create_jsproxy 및 create_jsproxy_with_flags 함수의 의사코드입니다.
def create_jsproxy(jsobj, jsthis=Js_undefined):
# For the definition of ``compute_type_flags``, see "Determining which flags to set".
return create_jsproxy_with_flags(compute_type_flags(jsobj), jsobj, jsthis)
def create_jsproxy_with_flags(type_flags, jsobj, jsthis):
cls = get_jsproxy_class(type_flags)
return cls.__new__(jsobj, jsthis)
가장 중요한 로직은 클래스를 생성하는 부분이며, 대략 다음과 같이 작동합니다.
@functools.cache
def get_jsproxy_class(type_flags):
flag_mixin_pairs = [
(HAS_GET, JSProxyHasGetMixin),
(HAS_HAS, JSProxyHasHasMixin),
# ...
(IS_PY_JSON_DICT, JSPyJsonDictMixin)
]
bases = [mixin for flag, mixin in flag_mixin_pairs if flag & type_flags]
bases.insert(0, JSProxy)
if type_flags & IS_ERROR:
# We want JSException to be pickleable so it needs a distinct name
name = "jstypes.ffi.JSException"
bases.append(Exception)
else:
name = "jstypes.ffi.JSProxy"
ns = {"_js_type_flags": type_flags}
# Note: The actual way that we build the class does not result in the
# mixins appearing as entries on the mro.
return JSProxyMeta.__new__(JSProxyMeta, name, tuple(bases), ns)
JSProxy 메타클래스
이 메타클래스는 한 JSProxy 클래스가 다른 JSProxy 클래스의 플래그를 모두 포함하면 해당 클래스를 서브클래스로 보고하도록 서브클래스 검사를 재정의합니다.
class _JSProxyMetaClass(type):
def __instancecheck__(cls, instance):
return cls.__subclasscheck__(type(instance))
def __subclasscheck__(cls, subcls):
if type.__subclasscheck__(cls, subcls):
return True
if not hasattr(subclass, "_js_type_flags"):
return False
subcls_flags = subcls._js_type_flags
# Check whether the flags on subcls are a subset of the flags on cls
return cls._js_type_flags & subcls_flags == subcls_flags
JSProxy 베이스 클래스
JSProxy 베이스 클래스에서 가장 복잡한 부분은 __getattribute__, __setattr__ 및 __delattr__의 구현입니다. __getattribute__의 경우 먼저 object.__getattribute__()를 호출하여 Python 객체 자체에 속성이 정의되어 있는지 확인합니다. 그렇지 않으면 JavaScript 객체에서 속성을 조회합니다.
__setattr__ 및 __delattr__의 경우 키 “__loader__”, “__name__”, “__package__”, “__path__”, “__spec__”를 Python 객체 자체에 설정합니다. 그 밖의 모든 값은 내부 JavaScript 객체에 설정하거나 내부 JavaScript 객체에서 삭제합니다. 이는 JavaScript 객체를 수정하지 않고도 Python 모듈로 사용할 수 있도록 하기 위한 것입니다.
특이한 특수 사례로 객체가 Array인 경우 keys 메서드를 걸러 냅니다. 또한 dir()의 결과에서도 해당 메서드를 제거합니다. 이는 JavaScript Array를 전달했을 때 dict.update()가 올바르게 동작하도록 하기 위한 것입니다. 다음과 같은 동작을 원합니다.
d = {}
d.update(run_js("[['a', 'b'], [1, 2]]"))
assert d == {"a" : "b", 1 : 2}
# The result if we didn't filter out Array.keys would be as follows:
assert d != {1 : ['a', 'b'], 2: [1, 2]}
가능한 대안은 dict.update()에 JavaScript 배열을 위한 특수 사례 처리를 추가하도록 하는 것입니다.
JavaScript 객체에는 Python 키워드와 이름이 같은 중요한 메서드가 있는 경우가 흔합니다(예: Array.from, Promise.then). Python에서는 유효한 식별자인 from_ 및 then_을 사용하여 이러한 메서드에 접근합니다. then_라는 JavaScript 속성에 접근하려면 then__에서 접근하는 식으로 처리합니다. 따라서 속성이 Python 키워드 뒤에 하나 이상의 밑줄이 오는 형태라면 끝에서 밑줄 하나를 제거합니다. 다음 헬퍼 함수가 이를 위해 사용됩니다.
def normalize_python_keywords(attr):
stripped = attr.strip("_")
if not keyword.iskeyword(stripped):
return attr
if stripped != attr:
return attr[:-1]
return attr
__bool__을 구현하려면 다음 JavaScript 함수가 필요합니다. JavaScript에서는 빈 컨테이너가 참으로 평가되지만 Python에서는 거짓으로 평가되어야 하므로, 빈 컨테이너를 감지하고 false를 반환합니다.
function js_bool(val) {
// if it's a falsey JS object, return false
if (!val) {
return false;
}
// We also want to return false on container types with size 0.
if (val.size === 0) {
// Return true for HTML elements even if they have a size of zero.
if (val instanceof HTMLElement) {
return true;
}
return false;
}
// A function with zero arguments has a length property equal to
// zero. Make sure we return true for this.
if (val.length === 0 && Array.isArray(val)) {
return false;
}
// An empty buffer
if (val.byteLength === 0) {
return false;
}
return true;
}
다음 보조 함수는 __dir__를 구현하는 데 사용합니다. 프로토타입 체인을 순회하면서 모든 키를 누적하고, 숫자로 시작하는 키(유효한 Python 식별자가 아님)를 제외하며 normalize_python_keywords 변환을 되돌립니다. 또한 Array.keys 메서드를 제외합니다.
function js_dir(jsobj) {
let result = [];
let orig = jsobj;
do {
let keys = Object.getOwnPropertyNames(jsobj);
result.push(...keys);
} while ((jsobj = Object.getPrototypeOf(jsobj)));
// Filter out numbers
result = result.filter((s) => {
let c = s.charCodeAt(0);
return c < 48 || c > 57;
});
// Filter out "keys" key from an array
if (Array.isArray(orig)) {
result = result.filter((s) => {
return s !== "keys";
});
}
// If the key is a keyword followed by 0 or more underscores,
// add an extra underscore to reverse the transformation applied by
// normalize_python_keywords().
result = result.map((word) =>
iskeyword(word.replace(/_*$/, "")) ? word + "_" : word,
);
return result;
};
class JSProxy:
def __getattribute__(self, attr):
try:
return object.__getattribute__(self, attr)
except AttributeError:
pass
if attr == "keys" and Array.isArray(self):
raise AttributeError(attr)
attr = normalize_python_keywords(attr)
js_getattr = run_js(
"""
(jsobj, attr) => jsobj[attr]
"""
)
js_hasattr = run_js(
"""
(jsobj, attr) => attr in jsobj
"""
)
result = js_getattr(self, attr)
if isjsfunction(result):
result = result.__get__(self)
if result is None and not js_hasattr(self, attr):
raise AttributeError(attr)
return result
def __setattr__(self, attr, value):
if attr in ["__loader__", "__name__", "__package__", "__path__", "__spec__"]:
return object.__setattr__(self, attr, value)
attr = normalize_python_keywords(attr)
js_setattr = run_js(
"""
(jsobj, attr) => {
jsobj[attr] = value;
}
"""
)
js_setattr(self, attr, value)
def __delattr__(self, attr):
if attr in ["__loader__", "__name__", "__package__", "__path__", "__spec__"]:
return object.__delattr__(self, attr)
attr = normalize_python_keywords(attr)
js_delattr = run_js(
"""
(jsobj, attr) => {
delete jsobj[attr];
}
"""
)
js_delattr(self, attr)
def __dir__(self):
return object.__dir__(self) + js_dir(self)
def __eq__(self, other):
if not isinstance(other, JSProxy):
return False
js_eq = run_js("(x, y) => x === y")
return js_eq(self, other)
def __ne__(self, other):
if not isinstance(other, JSProxy):
return True
js_neq = run_js("(x, y) => x !== y")
return js_neq(self, other)
def __repr__(self):
js_repr = run_js("x => x.toString()")
return js_repr(self)
def __bool__(self):
return js_bool(self)
@property
def js_id(self):
"""
This returns an integer with the property that jsproxy1 == jsproxy2
if and only if jsproxy1.js_id == jsproxy2.js_id. There is no way to
express the implementation in pseudocode.
"""
raise NotImplementedError
def as_py_json(self):
"""
This is actually a mixin method. We leave it out if any of the flags
IS_CALLABLE, IS_DOUBLE_PROXY, IS_ERROR, or IS_ITERATOR
is set.
"""
flags = self._js_type_flags
if (flags & (IS_ARRAY | IS_ARRAY_LIKE)):
flags |= IS_PY_JSON_SEQUENCE
else:
flags |= IS_PY_JSON_DICT
return create_jsproxy_with_flags(flags, self, self.jsthis)
def to_py(self, *, depth=-1, default_converter=None):
"""
See section on deep conversions.
"""
...
def object_entries(self):
js_object_entries = run_js("x => Object.entries(x)")
return js_object_entries(self)
def object_keys(self):
js_object_keys = run_js("x => Object.keys(x)")
return js_object_keys(self)
def object_values(self):
js_object_values = run_js("x => Object.values(x)")
return js_object_values(self)
def to_weakref(self):
js_weakref = run_js("x => new WeakRef(x)")
return js_weakref(self)
value에 as_py_json() 메서드가 있으면 이를 호출하는 다음 함수가 필요합니다:
def maybe_as_py_json(value):
if (
isinstance(value, JSProxy)
and hasattr(value, as_py_json)
):
return value.as_py_json()
return value
설정할 플래그 결정
다음 보조 함수 getTypeTag가 필요합니다:
function getTypeTag(x) {
try {
return Object.prototype.toString.call(x);
} catch (e) {
// Catch and ignore errors
return "";
}
}
어떤 플래그를 설정할지 결정하기 위해 다음 함수를 사용합니다:
function compute_type_flags(obj, is_py_json) {
let type_flags = 0;
const typeTag = getTypeTag(obj);
const hasLength =
isArray || (hasProperty(obj, "length") && typeof obj !== "function");
SET_FLAG_IF_HAS_METHOD(HAS_GET, "get");
SET_FLAG_IF_HAS_METHOD(HAS_SET, "set");
SET_FLAG_IF_HAS_METHOD(HAS_HAS, "has");
SET_FLAG_IF_HAS_METHOD(HAS_INCLUDES, "includes");
SET_FLAG_IF(
HAS_LENGTH,
hasProperty(obj, "size") || hasLength
);
SET_FLAG_IF_HAS_METHOD(HAS_DISPOSE, Symbol.dispose);
SET_FLAG_IF(IS_CALLABLE, typeof obj === "function");
SET_FLAG_IF(IS_ARRAY, Array.isArray(obj));
SET_FLAG_IF(
IS_ARRAY_LIKE,
!isArray && hasLength && (type_flags & IS_ITERABLE));
SET_FLAG_IF(IS_DOUBLE_PROXY, isPyProxy(obj));
SET_FLAG_IF(IS_GENERATOR, typeTag === "[object Generator]");
SET_FLAG_IF_HAS_METHOD(IS_ITERABLE, Symbol.iterator);
SET_FLAG_IF(
IS_ERROR,
hasProperty(obj, "name") &&
hasProperty(obj, "message") &&
(hasProperty(obj, "stack") || constructorName === "DOMException") &&
!(type_flags & IS_CALLABLE)
);
if (is_py_json && type_flags & (IS_ARRAY | IS_ARRAY_LIKE)) {
type_flags |= IS_PY_JSON_SEQUENCE;
} else if (
is_py_json &&
!(type_flags & (IS_DOUBLE_PROXY | IS_ITERATOR | IS_CALLABLE | IS_ERROR))
) {
type_flags |= IS_PY_JSON_DICT;
}
const mapping_flags = HAS_GET | HAS_LENGTH | IS_ITERABLE;
const mutable_mapping_flags = mapping_flags | HAS_SET;
SET_FLAG_IF(IS_MAPPING, type_flags & (mapping_flags === mapping_flags));
SET_FLAG_IF(
IS_MUTABLE_MAPPING,
type_flags & (mutable_mapping_flags === mutable_mapping_flags),
);
SET_FLAG_IF(IS_MAPPING, type_flags & IS_PY_JSON_DICT);
SET_FLAG_IF(IS_MUTABLE_MAPPING, type_flags & IS_PY_JSON_DICT);
return type_flags;
}
HAS_GET 믹스인
JavaScript get() 메서드가 있으면 다음과 같이 __getitem__을 정의합니다. has() 메서드도 있으면 undefined 반환 값을 키 오류로 처리할지 None으로 처리할지 결정하는 데 사용합니다. has() 메서드가 없으면 undefined는 None으로 처리합니다.
function js_get(jsobj, item) {
const result = jsobj.get(item);
if (result !== undefined) {
return result;
}
if (hasMethod(obj, "has") && !obj.has(key)) {
throw new PythonKeyError(item);
}
return undefined;
}
class JSProxyHasGetMixin:
def __getitem__(self, item):
result = js_get(self, item)
if self._js_type_flags & IS_PY_JSON_DICT:
result = maybe_as_py_json(result)
return result
HAS_SET 믹스인
set() 메서드가 있으면 delete() 메서드도 있다고 가정하고 다음과 같이 __setitem__과 __delitem__을 정의합니다:
class JSProxyHasSetMixin:
def __setitem__(self, item, value):
js_set = run_js(
"""
(jsobj, item, value) => {
jsobj.set(item, value);
}
"""
)
js_set(self, item, value)
def __delitem__(self, item, value):
js_delete = run_js(
"""
(jsobj, item) => {
jsobj.delete(item);
}
"""
)
js_delete(self, item)
HAS_HAS 믹스인
class JSProxyHasHasMixin:
def __contains__(self, item):
js_has = run_js(
"""
(jsobj, item) => jsobj.has(item);
"""
)
return js_has(self, item)
HAS_INCLUDES 믹스인
class JSProxyHasIncludesMixin:
def __contains__(self, item):
js_includes = run_js(
"""
(jsobj, item) => jsobj.includes(item);
"""
)
return js_includes(self, item)
HAS_LENGTH 믹스인
size 속성이 존재하고 숫자이면 이를 우선 사용하고, 그렇지 않으면 length를 반환합니다. 두 필드 중 하나를 조회할 때 JavaScript 오류가 발생하면 이를 JavaScriptException으로 Python에 전파합니다.
class JSProxyHasLengthMixin:
def __len__(self, item):
js_len = run_js(
"""
(jsobj) => {
const size = val.size;
if (typeof size === "number") {
return size;
}
return val.length
}
"""
)
result = js_len(self)
if not isinstance(result, int):
raise TypeError("object does not have a valid length")
if result < 0:
raise ValueError("length of object is negative")
return result
HAS_DISPOSE 믹스인
이렇게 하면 JSProxy가 컨텍스트 관리자가 되며, __enter__는 아무 작업도 하지 않고 __exit__는 [Symbol.dispose]() 메서드를 호출합니다.
class JSProxyContextManagerMixin:
def __enter__(self):
return self
def __exit__(self, type, value, traceback):
js_symbol_dispose = run_js(
"""
(jsobj) => jsobj[Symbol.dispose]()
"""
)
js_symbol_dispose(self)
IS_ARRAY 믹스인
function js_array_slice(jsobj, length, start, stop, step) {
let result;
if (step === 1) {
result = obj.slice(start, stop);
} else {
result = Array.from({ length }, (_, i) => obj[start + i * step]);
}
return result;
}
// we also use this for deletion by setting values to None
function js_array_slice_assign(obj, slicelength, start, stop, step, values) {
if (step === 1) {
obj.splice(start, slicelength, ...(values ?? []));
return;
}
if (values !== undefined) {
for (let i = 0; i < slicelength; i++) {
obj.splice(start + i * step, 1, values[i]);
}
}
for (let i = slicelength - 1; i >= 0; i --) {
obj.splice(start + i * step, 1);
}
}
class JSArrayMixin(MutableSequence, JSProxyHasLengthMixin):
def __getitem__(self, index):
if not isinstance(index, (int, slice)):
raise TypeError("Expected index to be an int or a slice")
length = len(self)
js_array_get = run_js(
"""
(jsobj, index) => jsobj[index]
"""
)
if isinstance(index, int):
if index >= length:
raise IndexError(index)
if index < -length:
raise IndexError(index)
if index < 0:
index += length
result = js_array_get(self, index)
if self._js_type_flags & IS_PY_JSON_SEQUENCE:
result = maybe_as_py_json(result)
return result
start = index.start
stop = index.stop
step = index.step
slicelength = PySlice_AdjustIndices(length, &start, &stop, &step)
if (slicelength <= 0) {
return _PyJsvArray_New();
}
result = js_array_slice(self, slicelength, start, stop, step)
if self._js_type_flags & IS_PY_JSON_SEQUENCE:
result = result.as_py_json()
return result
def __setitem__(self, index, value):
if not isinstance(index, (int, slice)):
raise TypeError("Expected index to be an int or a slice")
length = len(self)
js_array_set = run_js(
"""
(jsobj, index, value) => { jsobj[index] = value; }
"""
)
if isinstance(index, int):
if index >= length:
raise IndexError(index)
if index < -length:
raise IndexError(index)
if index < 0:
index += length
result = js_array_set(self, index, value)
return
if not isinstance(value, Iterable):
raise TypeError("must assign iterable to extended slice")
seq = list(value)
start = index.start
stop = index.stop
step = index.step
slicelength = PySlice_AdjustIndices(length, &start, &stop, &step)
if step != 1 and len(seq) != slicelength:
raise TypeError(
f"attempted to assign sequence of length {len(seq)} to"
f"extended slice of length {slicelength}"
)
if step != 1 and slicelength == 0:
return
js_array_slice_assign(self, slicelength, start, stop, step, seq)
def __delitem__(self, index):
if not isinstance(index, (int, slice)):
raise TypeError("Expected index to be an int or a slice")
length = len(self)
js_array_delete = run_js(
"""
(jsobj, index) => { jsobj.splice(index, 1); }
"""
)
if isinstance(index, int):
if index >= length:
raise IndexError(index)
if index < -length:
raise IndexError(index)
if index < 0:
index += length
result = js_array_delete(self, index)
return
start = index.start
stop = index.stop
step = index.step
slicelength = PySlice_AdjustIndices(length, &start, &stop, &step)
if step != 1 and slicelength == 0:
return
js_array_slice_assign(self, slicelength, start, stop, step, None)
def insert(self, pos, value):
if not isinstance(pos, int):
raise TypeError("Expected an integer")
js_insert = run_js(
"""
(jsarr, pos, value) => { jsarr.splice(pos, value); }
"""
)
js_insert(self, pos, value)
IS_ARRAY_LIKE 믹스인
class JSArrayLikeMixin(MutableSequence, JSProxyHasLengthMixin):
def __getitem__(self, index):
if not isinstance(index, int):
raise TypeError("Expected index to be an int")
JSArrayMixin.__getitem__(self, index)
def __setitem__(self, index, value):
if not isinstance(index, int):
raise TypeError("Expected index to be an int")
JSArrayMixin.__setitem__(self, index, value)
def __delitem__(self, index):
if not isinstance(index, int):
raise TypeError("Expected index to be an int")
JSArrayMixin.__delitem__(self, index, value)
IS_CALLABLE 믹스인
JSCallable을 호출하는 더 정확한 C 코드는 이미 제시했습니다. 특히 그곳에 제시한 JSMethod_ConvertArgs()의 정의를 참조하십시오.
class JSCallableMixin:
def __get__(self, obj):
"""Return a new jsproxy bound to jsthis with the same JS object"""
return create_jsproxy(self, jsthis=obj)
def __call__(self, *args, **kwargs):
"""See the description of JSMethod_Vectorcall"""
def new(self, *args, **kwargs):
pyproxies = []
jsargs = JSMethod_ConvertArgs(args, kwargs, pyproxies)
do_construct = run_js(
"""
(jsfunc, jsargs) =>
Reflect.construct(jsfunc, jsargs)
"""
)
result = do_construct(self, jsargs)
msg = (
"This borrowed proxy was automatically destroyed "
"at the end of a function call."
)
for px in pyproxies:
px.destroy(msg)
return result
IS_ERROR 믹스인
이 경우 Exception과 JSProxy를 모두 상속합니다. 또한 결과 클래스가 피클 가능하도록 보장합니다.
IS_ITERABLE 믹스인
이터러블에 IS_PY_JSON_DICT 플래그가 설정되어 있으면 객체 키를 순회합니다. 그렇지 않으면 obj[Symbol.iterator]()를 호출합니다. IS_PY_JSON_SEQUENCE 또는 IS_PY_JSON_DICT 플래그 중 하나가 설정되어 있으면 반복 결과에 maybe_as_py_json을 호출합니다.
def wrap_with_maybe_as_py_json(it):
try:
while val := it.next()
yield maybe_as_py_json(val)
except StopIteration(result):
return maybe_as_py_json(result)
class JSIterableMixin:
def __iter__(self):
pyjson = self._js_type_flags & (IS_PY_JSON_SEQUENCE | IS_PY_JSON_DICT)
pyjson_dict = self._js_type_flags & IS_PY_JSON_DICT
js_get_iter = run_js(
"""
(obj) => obj[Symbol.iterator]()
"""
)
if pyjson_dict:
result = iter(self.object_keys())
else:
result = js_get_iter(self)
if pyjson:
result = wrap_with_maybe_as_py_json(result)
return result
IS_ITERATOR 믹스인
JavaScript의 next 메서드는 done 필드와 value 필드를 가진 IteratorResult를 반환합니다. done이 true이면 파이썬 이터레이터 프로토콜로 변환하기 위해 StopIteration예외를 발생시켜야 합니다.
class JSIteratorMixin:
def __iter__(self):
return self
def send(self, arg):
js_next = run_js(
"""
(obj, arg) => obj.next(arg)
"""
)
it_result = js_next(self, arg)
value = it_result.value
if it_result.done:
raise StopIteration(value)
return value
def __next__(self):
return self.send(None)
IS_GENERATOR 믹스인
파이썬 제너레이터에는 return() 메서드 대신 인자를 받지 않는 close()메서드가 있습니다. 또한 gen.throw(GeneratorExit)를 jsgen.return_()로 변환해야 합니다. 특정 값을 반환해야 하는 경우 jsgen.return_(val)을 직접 호출할 수 있습니다.
class JSGeneratorMixin(JSIteratorMixin):
def throw(self, exc):
if isinstance(exc, GeneratorExit):
js_throw = run_js(
"""
(obj, exc) => obj.return()
"""
)
else:
js_throw = run_js(
"""
(obj, exc) => obj.throw(exc)
"""
)
it_result = js_throw(self, exc)
# if the error wasn't caught it will get raised back out.
# now handle the case where the error got caught.
value = it_result.value
if self._js_type_flags & IS_PY_JSON_SEQUENCE:
value = maybe_as_py_json(value)
if it_result.done:
raise StopIteration(value)
return value
def close(self):
self.throw(GeneratorExit)
IS_MAPPING 믹스인
IS_MAPPING플래그가 설정되면 모든 Mapping메서드를 구현합니다. 추상 Mapping메서드가 정의될 만큼 다른 플래그가 충분히 설정된 경우에만 이 플래그를 설정합니다. 모든 믹스인 메서드에는 기본 구현을 사용합니다.
IS_MUTABLE_MAPPING 믹스인
IS_MUTABLE_MAPPING플래그가 설정되면 모든 MutableMapping메서드를 구현합니다. 추상 MutableMapping메서드가 정의될 만큼 다른 플래그가 충분히 설정된 경우에만 이 플래그를 설정합니다. 모든 믹스인 메서드에는 기본 구현을 사용합니다.
IS_PY_JSON_SEQUENCE 믹스인
이 플래그는 항상 IS_ARRAY와 함께 나타납니다. 이 플래그는 결과에 maybe_as_py_json()을 적용하도록 JSArray.__getitem__의 동작을 변경합니다.
IS_PY_JSON_DICT 믹스인
class JSPyJsonDictMixin(MutableMapping):
def __getitem__(self, key):
if not isinstance(key, str):
raise KeyError(key)
js_get = run_js(
"""
(jsobj, key) => jsobj[key]
"""
)
result = js_get(self, key)
if result is None and not key in self:
raise KeyError(key)
return maybe_as_py_json(result)
def __setitem__(self, key, value):
if not isinstance(key, str):
raise TypeError("only keys of type string are supported")
js_set = run_js(
"""
(jsobj, key, value) => {
jsobj[key] = value;
}
"""
)
js_set(self, key, value)
def __delitem__(self, key):
if not isinstance(key, str):
raise TypeError("only keys of type string are supported")
if not key in self:
raise KeyError(key)
js_delete = run_js(
"""
(jsobj, key) => {
delete jsobj[key];
}
"""
)
js_delete(self, key)
def __contains__(self, key):
if not isinstance(key, str):
return False
js_contains = run_js(
"""
(jsobj, key) => key in jsobj
"""
)
return js_contains(self, key)
def __len__(self):
return sum(1 for _ in self)
def __iter__(self):
# defined by IS_ITERABLE mixin, see implementation there.
IS_DOUBLE_PROXY 믹스인
이 경우 객체는 PyProxy의 JSProxy입니다. 내부 Python 객체를 반환하는 추가 unwrap()메서드를 추가합니다.
PyProxy
Python 객체가 지원할 수 있으며 해당 객체로부터 생성하는 PyProxy의 유형에 영향을 주는 믹스인 12개를 정의합니다.
HAS_GET- Python 객체에
__getitem__메서드가 있으면 이 플래그를 설정합니다. 해당 메서드가 존재하면 이를 사용하여PyProxy에get()메서드를 구현합니다. HAS_SET- Python 객체에
__setitem__메서드가 있으면 이 플래그를 설정합니다. 해당 메서드가 존재하면 이를 사용하여PyProxy에set()메서드를 구현합니다. HAS_CONTAINS- 파이썬 객체에
__contains__메서드가 있으면 이 플래그를 설정합니다. 있으면 이를 사용하여PyProxy에has()메서드를 구현합니다. HAS_LENGTH- 파이썬 객체에
__len__메서드가 있으면 이 플래그를 설정합니다. 있으면 이를 사용하여PyProxy에lengthgetter를 구현합니다. IS_CALLABLE- 파이썬 객체에
__call__메서드가 있으면 이 플래그를 설정합니다. 존재하는 경우PyProxy를 호출 가능 객체로 만듭니다. IS_DICT- 파이썬 객체의 정확한 타입이
dict인 경우 이 플래그를 설정합니다. 있으면getattr(pyobj, "some_property")가AttributeError를 발생시키는 경우pyproxy.some_property속성이pyobj.__getitem__("some_property")로 폴백되도록 합니다. IS_GENERATOR- 파이썬 객체가
collections.abc.Generator의 인스턴스인 경우 이 플래그를 설정합니다. 있으면PyProxy가 자바스크립트 제너레이터의 메서드를 구현하도록 합니다. IS_ITERABLE- 파이썬 객체에
__iter__메서드가 있으면 이 플래그를 설정합니다. 있으면 이를 사용하여PyProxy에[Symbol.iterator]메서드를 구현합니다. IS_ITERATOR- 파이썬 객체에
__next__메서드가 있으면 이 플래그를 설정합니다. 있으면 이를 사용하여PyProxy에next()메서드를 구현합니다. IS_SEQUENCE- 파이썬 객체가
collections.abc.Sequence의 인스턴스인 경우 이 플래그를 설정합니다. 설정되어 있으면 이를 사용하여PyProxy에서 변경하지 않는Array.prototype메서드를 모두 구현합니다. IS_MUTABLE_SEQUENCE- 파이썬 객체가
collections.abc.MutableSequence의 인스턴스인 경우 이 플래그를 설정합니다. 설정되어 있으면 이를 사용하여PyProxy에 모든Array.prototype메서드를 구현합니다. IS_JS_JSON_DICT- 딕셔너리에
asJsJson()메서드를 사용할 때 이 플래그를 설정합니다. 이 플래그가 설정되면PyProxy에 대한 속성 접근은 _only___getitem__에서 가져온 값만 조회하고 파이썬 객체의 속성은 조회하지 않습니다. 또한PyProxy를 인덱싱하거나 이터레이션한 결과에 대해서도asJsJson()를 호출합니다. IS_JS_JSON_SEQUENCESequence에서asJsJson()를 사용할 때 이 플래그를 설정합니다. 이 플래그가 설정되면PyProxy를 인덱싱하거나 반복한 결과에asJsJson()를 호출합니다.
PyProxy는 JavaScript 클래스와 ES6 Proxy 핸들러 모음이 혼합되어 구성됩니다. 어떤 플래그가 있는지에 따라 적절한 믹스인 모음과 적절한 핸들러를 선택하여 클래스를 구성합니다.
PyProxy가 생성되면 래핑된 Python 객체의 참조 횟수를 증가시킵니다. PyProxy가 소멸되면 참조 횟수를 감소시키고 소멸된 것으로 표시합니다. 그 결과 PyProxy로 무엇인가를 시도하면 해당 객체에 _Py_js2python()를 호출하고 오류가 발생합니다.
PyProxy생성
타입 플래그 모음이 주어지면 다음 함수를 사용하여 PyProxy 클래스를 생성합니다.
let pyproxyClassMap = new Map();
function getPyProxyClass(flags: number) {
let result = pyproxyClassMap.get(flags);
if (result) {
return result;
}
let descriptors: any = {};
const FLAG_MIXIN_PAIRS: [number, any][] = [
[HAS_CONTAINS, PyContainsMixin],
// ... other flag mixin pairs
[IS_MUTABLE_SEQUENCE, PyMutableSequenceMixin],
];
for (let [feature_flag, methods] of FLAG_MIXIN_PAIRS) {
if (flags & feature_flag) {
Object.assign(
descriptors,
Object.getOwnPropertyDescriptors(methods.prototype),
);
}
}
// Use base constructor (just throws an error if construction is attempted).
descriptors.constructor = Object.getOwnPropertyDescriptor(
PyProxyProto,
"constructor",
);
// $$flags static field
Object.assign(
descriptors,
Object.getOwnPropertyDescriptors({ $$flags: flags }),
);
// We either inherit PyProxyFunction as the base class if we're callable or
// from PyProxy if we're not.
const superProto = flags & IS_CALLABLE ? PyProxyFunctionProto : PyProxyProto;
const subProto = Object.create(superProto, descriptors);
function NewPyProxyClass() {}
NewPyProxyClass.prototype = subProto;
pyproxyClassMap.set(flags, NewPyProxyClass);
return NewPyProxyClass;
}
PyProxy를 생성하려면 적절한 핸들러를 가져올 수 있어야 합니다.
function getPyProxyHandlers(flags) {
if (flags & IS_JS_JSON_DICT) {
return PyProxyJsJsonDictHandlers;
}
if (flags & IS_DICT) {
return PyProxyDictHandlers;
}
if (flags & IS_SEQUENCE) {
return PyProxySequenceHandlers;
}
return PyProxyHandlers;
}
다음 함수를 사용하여 ES6 프록시의 대상 객체를 생성합니다.
function createTarget(flags) {
const pyproxyClass = getPyProxyClass(flags);
if (!(flags & IS_CALLABLE)) {
return Object.create(cls.prototype);
}
// In this case we are effectively subclassing Function in order to ensure
// that the proxy is callable. With a Content Security Protocol that doesn't
// allow unsafe-eval, we can't invoke the Function constructor directly. So
// instead we create a function in the universally allowed way and then use
// `setPrototypeOf`. The documentation for `setPrototypeOf` says to use
// `Object.create` or `Reflect.construct` instead for performance reasons
// but neither of those work here.
const target = function () {};
Object.setPrototypeOf(target, cls.prototype);
// Remove undesirable properties added by Function constructor. Note: we
// can't remove "arguments" or "caller" because they are not configurable
// and not writable
delete target.length;
delete target.name;
// prototype isn't configurable so we can't delete it but it is writable.
target.prototype = undefined;
return target;
}
createPyProxy는 다음 옵션을 사용합니다.
- flags
- 이 옵션이 전달되면 객체의 기능을 다시 감지하는 대신 전달된 플래그를 사용합니다.
- props
- 동일한 수명을 가진 다른 PyProxy와 공유되지 않는 정보입니다.
- shared
- 이 프록시와 수명이 동일한 모든 프록시 간에 공유되는 데이터입니다.
- gcRegister
- JavaScript 가비지 컬렉터에 이를 등록할지 여부를 지정합니다.
const pyproxyAttrsSymbol = Symbol("pyproxy.attrs");
function createPyProxy(
pyObjectPtr: number,
{
flags,
props,
shared,
gcRegister,
}
) {
if (gcRegister === undefined) {
// register by default
gcRegister = true;
}
// See the section "Determining which flags to set" for the definition of
// get_pyproxy_flags
const pythonGetFlags = makePythonFunction("get_pyproxy_flags");
flags ??= pythonGetFlags(pyObjectPtr);
const target = createTarget(flags);
const handlers = getPyProxyHandlers(flags);
const proxy = new Proxy(target, handlers);
props = Object.assign(
{ isBound: false, captureThis: false, boundArgs: [], roundtrip: false },
props,
);
// If shared was passed the new PyProxy will have a shared lifetime
// with some other PyProxy.
// This happens in asJsJson(), bind(), and captureThis().
// It specifically does not happen in copy()
if (!shared) {
shared = {
pyObjectPtr,
destroyed_msg: undefined,
gcRegistered: false,
};
_Py_IncRef(pyObjectPtr);
if (gcRegister) {
gcRegisterPyProxy(shared);
}
}
target[pyproxyAttrsSymbol] = { shared, props };
return proxy;
}
PyProxy 기본 클래스
기본 핸들러는 다음과 같습니다.
function filteredHasKey(jsobj, jskey, filterProto) {
let result = jskey in jsobj;
if (jsobj instanceof Function) {
// If we are a PyProxy of a callable we have to subclass function so that if
// someone feature detects callables with `instanceof Function` it works
// correctly. But the callable might have attributes `name` and `length` and
// we don't want to shadow them with the values from `Function.prototype`.
result &&= !(
["name", "length", "caller", "arguments"].includes(jskey) ||
// we are required by JS law to return `true` for `"prototype" in pycallable`
// but we are allowed to return the value of `getattr(pycallable, "prototype")`.
// So we filter prototype out of the "get" trap but not out of the "has" trap
(filterProto && jskey === "prototype")
);
}
return result;
}
const PyProxyHandlers = {
isExtensible() {
return true;
},
has(jsobj, jskey) {
// Must report "prototype" in proxy when we are callable.
// (We can return the wrong value from "get" handler though.)
if (filteredHasKey(jsobj, jskey, false)) {
return true;
}
// hasattr will crash if given a Symbol.
if (typeof jskey === "symbol") {
return false;
}
if (jskey.startsWith("$")) {
jskey = jskey.slice(1);
}
const pythonHasAttr = makePythonFunction("hasattr");
return pythonHasAttr(jsobj, jskey);
},
get(jsobj, jskey) {
// Preference order:
// 1. stuff from JavaScript
// 2. the result of Python getattr
// pythonGetAttr will crash if given a Symbol.
if (typeof jskey === "symbol" || filteredHasKey(jsobj, jskey, true)) {
return Reflect.get(jsobj, jskey);
}
if (jskey.startsWith("$")) {
jskey = jskey.slice(1);
}
// 2. The result of getattr
const pythonGetAttr = makePythonFunction("getattr");
return pythonGetAttr(jsobj, jskey);
},
set(jsobj, jskey, jsval) {
let descr = Object.getOwnPropertyDescriptor(jsobj, jskey);
if (descr && !descr.writable && !descr.set) {
return false;
}
// pythonSetAttr will crash if given a Symbol.
if (typeof jskey === "symbol" || filteredHasKey(jsobj, jskey, true)) {
return Reflect.set(jsobj, jskey, jsval);
}
if (jskey.startsWith("$")) {
jskey = jskey.slice(1);
}
const pythonSetAttr = makePythonFunction("setattr");
pythonSetAttr(jsobj, jskey, jsval);
return true;
},
deleteProperty(jsobj, jskey: string | symbol): boolean {
let descr = Object.getOwnPropertyDescriptor(jsobj, jskey);
if (descr && !descr.configurable) {
// Must return "false" if "jskey" is a nonconfigurable own property.
// Strict mode JS will throw an error here saying that the property cannot
// be deleted.
return false;
}
if (typeof jskey === "symbol" || filteredHasKey(jsobj, jskey, true)) {
return Reflect.deleteProperty(jsobj, jskey);
}
if (jskey.startsWith("$")) {
jskey = jskey.slice(1);
}
const pythonDelAttr = makePythonFunction("delattr");
pythonDelAttr(jsobj, jskey);
return true;
},
ownKeys(jsobj) {
const pythonDir = makePythonFunction("dir");
const result = pythonDir(jsobj).toJs();
result.push(...Reflect.ownKeys(jsobj));
return result;
},
apply(jsobj: PyProxy & Function, jsthis: any, jsargs: any): any {
return jsobj.apply(jsthis, jsargs);
},
};
또한 기본 클래스에는 다음 메서드가 있습니다.
class PyProxy {
constructor() {
throw new TypeError("PyProxy is not a constructor");
}
get [Symbol.toStringTag]() {
return "PyProxy";
}
static [Symbol.hasInstance](obj: any): obj is PyProxy {
return [PyProxy, PyProxyFunction].some((cls) =>
Function.prototype[Symbol.hasInstance].call(cls, obj),
);
}
get type() {
const pythonType = makePythonFunction(`
def python_type(obj):
ty = type(obj)
if ty.__module__ in ['builtins', 'main']:
return ty.__name__
return ty.__module__ + "." + ty.__name__
`);
return pythonType(this);
}
toString() {
const pythonStr = makePythonFunction("str");
return pythonStr(this);
}
destroy(options) {
const { shared } = proxy[pyproxyAttrsSymbol];
if (!shared.pyObjectPtr) {
// already destroyed
return;
}
shared.pyObjectPtr = 0;
shared.destroyed_msg = options.message ?? "Object has already been destroyed";
_Py_DecRef(shared.pyObjectPtr);
}
[Symbol.dispose]() {
this.destroy();
}
copy() {
const { shared, props } = proxy[pyproxyAttrsSymbol];
// Don't pass shared as an option since we want this new PyProxy to
// have a distinct lifetime from the one we are copying.
return createPyProxy(shared.pyObjectPtr, {
flags: this.$$flags,
props: attrs.props,
});
}
toJs(options) {
// See the definition of to_js in "Deep conversions".
}
}
설정할 플래그 결정
이를 타입에만 의존하는 플래그를 계산하는 get_type_flags 컴포넌트와 PyProxy에 hasBeenJsJson이 있는지 여부에도 의존하는 컴포넌트로 분리합니다.
def get_type_flags(ty):
from collections.abc import Generator, MutableSequence, Sequence
flags = 0
if hasattr(ty, "__len__"):
flags |= HAS_LENGTH
if hasattr(ty, "__getitem__"):
flags |= HAS_GET
if hasattr(ty, "__setitem__"):
flags |= HAS_SET
if hasattr(ty, "__contains__"):
flags |= HAS_CONTAINS
if ty is dict:
# Currently we don't set this on subclasses.
flags |= IS_DICT
if hasattr(ty, "__call__"):
flags |= IS_CALLABLE
if hasattr(ty, "__iter__"):
flags |= IS_ITERABLE
if hasattr(ty, "__next__"):
flags |= IS_ITERATOR
if issubclass(ty, Generator):
flags |= IS_GENERATOR
if issubclass(ty, Sequence):
flags |= IS_SEQUENCE
if issubclass(ty, MutableSequence):
flags |= IS_MUTABLE_SEQUENCE
return flags
def get_pyproxy_flags(obj, is_js_json):
flags = get_type_flags(type(obj))
if not is_js_json:
return flags
if flags & IS_SEQUENCE:
flags |= IS_JS_JSON_SEQUENCE
elif flags & HAS_GET:
flags |= IS_JS_JSON_DICT
return flags
HAS_GET 믹스인
const pythonGetItem = makePythonFunction(`
def getitem(obj, key):
return obj[key]
`);
class PyProxyGetItemMixin {
get(key) {
let result = pythonGetItem(this, key);
const isJsJson = !!(this.$$flags & (IS_JS_JSON_DICT | IS_JS_JSON_SEQUENCE));
if (isJsJson && result.asJsJson) {
result = result.asJsJson();
}
return result;
}
asJsJson() {
const flags = this.$$flags | IS_JS_JSON_DICT;
const { shared, props } = this[pyproxyAttrsSymbol];
// Note: The PyProxy created here has the same lifetime as the PyProxy it is
// created from. Destroying either destroys both.
return createPyProxy(shared.ptr, { flags, shared, props });
}
}
HAS_SET 믹스인
class PyProxySetItemMixin {
set(key, value) {
const pythonSetItem = makePythonFunction(`
def setitem(obj, key, value):
obj[key] = value
`);
pythonSetItem(this, key, value);
}
delete(key) {
const pythonDelItem = makePythonFunction(`
def delitem(obj, key):
del obj[key]
`);
pythonDelItem(this, key);
}
}
HAS_CONTAINS 믹스인
const pythonHasItem = makePythonFunction(`
def hasitem(obj, key):
return key in obj
`);
class PyContainsMixin {
has(key) {
return pythonHasItem(this, key);
}
}
HAS_LENGTH 믹스인
const pythonLength = makePythonFunction("len");
class PyLengthMixin {
get length() : number {
return pythonLength(this);
}
}
IS_CALLABLE 믹스인
PyProxy와 Function 모두를 상속하도록 사용자 지정 프로토타입과 클래스를 만들어야 합니다.
const PyProxyFunctionProto = Object.create(
Function.prototype,
Object.getOwnPropertyDescriptors(PyProxy.prototype),
);
function PyProxyFunction() {}
PyProxyFunction.prototype = PyProxyFunctionProto;
captureThis가 true이면 this를 첫 번째 인자로 삽입하고 바인딩된 인자를 추가하는 다음 헬퍼 함수를 사용합니다.
function _adjustArgs(pyproxy, jsthis, jsargs) {
const { props } = this[pyproxyAttrsSymbol];
const { captureThis, boundArgs, boundThis, isBound } = props;
if (captureThis) {
if (isBound) {
return [boundThis].concat(boundArgs, jsargs);
} else {
return [jsthis].concat(jsargs);
}
}
if (isBound) {
return boundArgs.concat(jsargs);
}
return jsargs;
}
이어서 다음 메서드를 구현합니다. apply(), call(), and bind()는 Function.prototype의 메서드입니다. callKwargs()와 captureThis()는 PyProxy에만 해당합니다.
export class PyCallableMixin {
apply(thisArg, jsargs) {
// Convert jsargs to an array using ordinary .apply in order to match the
// behavior of .apply very accurately.
jsargs = function (...args) {
return args;
}.apply(undefined, jsargs);
jsargs = _adjustArgs(this, thisArg, jsargs);
const pyObjectPtr = this[pyproxyAttrsSymbol].shared.pyObjectPtr;
return callPyObjectKwargs(pyObjectPtr, jsargs, {});
}
call(thisArg, ...jsargs) {
jsargs = _adjustArgs(this, thisArg, jsargs);
const pyObjectPtr = this[pyproxyAttrsSymbol].shared.pyObjectPtr;
return callPyObjectKwargs(pyObjectPtr, jsargs, {});
}
/**
* Call the function with keyword arguments. The last argument must be an
* object with the keyword arguments.
*/
callKwargs(...jsargs) {
jsargs = _adjustArgs(this, thisArg, jsargs);
if (jsargs.length === 0) {
throw new TypeError(
"callKwargs requires at least one argument (the kwargs object)",
);
}
let kwargs = jsargs.pop();
if (
kwargs.constructor !== undefined &&
kwargs.constructor.name !== "Object"
) {
throw new TypeError("kwargs argument is not an object");
}
const pyObjectPtr = this[pyproxyAttrsSymbol].shared.pyObjectPtr;
return callPyObjectKwargs(pyObjectPtr, jsargs, kwargs);
}
/**
* This is our implementation of Function.prototype.bind().
*/
bind(thisArg, ...jsargs) {
let { shared, props } = this[pyproxyAttrsSymbol];
const { boundArgs: boundArgsOld, boundThis: boundThisOld, isBound } = props;
let boundThis = thisArg;
if (isBound) {
boundThis = boundThisOld;
}
const boundArgs = boundArgsOld.concat(jsargs);
props = Object.assign({}, props, {
boundArgs,
isBound: true,
boundThis,
});
return createPyProxy(shared.ptr, {
shared,
flags: this.$$flags,
props,
});
}
/**
* This method makes a new PyProxy where ``this`` is passed as the
* first argument to the Python function. The new PyProxy has the
* same lifetime as the original.
*/
captureThis() {
let { props, shared } = this[pyproxyAttrsSymbol];
props = Object.assign({}, props, {
captureThis: true,
});
return createPyProxy(shared.ptr, {
shared,
flags: this.$$flags,
props,
});
}
}
IS_DICT 믹스인
IS_DICT 믹스인은 추가 메서드를 포함하지 않지만, 특수한 핸들러 집합을 사용합니다. 이러한 핸들러는 일반 핸들러와 JS_JSON_DICT 핸들러의 혼합형입니다. 먼저 hasattr(d, property)를 확인하고, 참이면 d.property를 반환합니다. 그렇지 않으면 d.get(property, None)을 반환합니다. 나머지 메서드도 모두 동일하게 작동합니다. 해당 핸들러의 정의는 IS_JS_JSON_DICT 플래그를 참조하십시오.
const PyProxyDictHandlers = {
isExtensible(): boolean {
return true;
},
has(jsobj: PyProxy, jskey: string | symbol): boolean {
if (PyProxyHandlers.has(jsobj, jskey)) {
return true;
}
return PyProxyJsJsonDictHandlers.has(jsobj, jskey);
},
get(jsobj: PyProxy, jskey: string | symbol): any {
let result = PyProxyHandlers.get(jsobj, jskey);
if (result !== undefined || PyProxyHandlers.has(jsobj, jskey)) {
return result;
}
return PyProxyJsJsonDictHandlers.get(jsobj, jskey);
},
set(jsobj: PyProxy, jskey: string | symbol, jsval: any): boolean {
if (PyProxyHandlers.has(jsobj, jskey)) {
return PyProxyHandlers.set(jsobj, jskey, jsval);
}
return PyProxyJsJsonDictHandlers.set(jsobj, jskey, jsval);
},
deleteProperty(jsobj: PyProxy, jskey: string | symbol): boolean {
if (PyProxyHandlers.has(jsobj, jskey)) {
return PyProxyHandlers.deleteProperty(jsobj, jskey);
}
return PyProxyJsJsonDictHandlers.deleteProperty(jsobj, jskey);
},
getOwnPropertyDescriptor(jsobj: PyProxy, prop: any) {
return (
Reflect.getOwnPropertyDescriptor(jsobj, prop) ??
PyProxyJsJsonDictHandlers.getOwnPropertyDescriptor(jsobj, prop)
);
},
ownKeys(jsobj: PyProxy): (string | symbol)[] {
const result = [
...PyProxyHandlers.ownKeys(jsobj),
...PyProxyJsJsonDictHandlers.ownKeys(jsobj)
];
// deduplicate
return Array.from(new Set(result));
},
};
IS_ITERABLE 믹스인
const pythonNext = makePythonFunction("next");
const getStopIterationValue = makePythonFunction(`
def get_stop_iteration_value():
import sys
err = sys.last_value
return err.value
`);
function* iterHelper(iter, isJsJson) {
try {
while (true) {
let item = pythonNext(iter);
if (isJsJson && item.asJsJson) {
item = item.asJsJson();
}
yield item;
}
} catch (e) {
if (e.type === "StopIteration") {
return getStopIterationValue();
}
throw e;
}
}
const pythonIter = makePythonFunction("iter");
class PyIterableMixin {
[Symbol.iterator]() {
const isJsJson = !!(this.$$flags & (IS_JS_JSON_DICT | IS_JS_JSON_SEQUENCE));
return iterHelper(pythonIter(this), isJsJson);
}
}
IS_ITERATOR 믹스인
const pythonSend = makePythonFunction(`
def python_send(it, val):
return gen.send(val)
`);
class PyIteratorMixin {
next(x) {
try {
const result = pythonSend(this, x);
return { done: false, value: result };
} catch (e) {
if (e.type === "StopIteration") {
const result = getStopIterationValue();
return { done: true, value: result };
}
throw e;
}
}
}
IS_GENERATOR 믹스인
const pythonThrow = makePythonFunction(`
def python_throw(gen, val):
return gen.throw(val)
`);
const pythonClose = makePythonFunction(`
def python_close(gen):
return gen.close()
`);
class PyGeneratorMixin extends PyIteratorMixin {
throw(exc) {
try {
const result = pythonThrow(this, exc);
return { done: false, value: result };
} catch (e) {
if (e.type === "StopIteration") {
const result = getStopIterationValue();
return { done: true, value: result };
}
throw e;
}
}
return(value) {
pythonClose(this);
return { done: true, value }
}
}
IS_SEQUENCE 믹스인
시퀀스를 변경하지 않는 모든 Array.prototype의 메서드를 PySequenceMixin에 정의합니다. 대부분의 경우 Array 프로토타입 메서드는 변경 없이 작동합니다. 이러한 메서드는 모두 다음 형식의 보일러플레이트로 정의합니다.
[methodName](...args) {
return Array.prototype[methodName].call(this, ...args)
}
여기에는 join, slice, indexOf, lastIndexOf, forEach, map, filter, some, every, reduce, reduceRight, at, concat, includes, entries, keys, values, find, findIndex가 포함됩니다. 이러한 보일러플레이트 메서드를 제외한 PySequenceMixin의 나머지 속성은 다음과 같습니다.
class PySequenceMixin {
get [Symbol.isConcatSpreadable]() {
return true;
}
toJSON() {
return Array.from(this);
}
asJsJson() {
const flags = this.$$flags | IS_JS_JSON_SEQUENCE;
const { shared, props } = this[pyproxyAttrsSymbol];
// Note: Because we pass shared down, the PyProxy created here has
// the same lifetime as the PyProxy it is created from. Destroying
// either destroys both.
return createPyProxy(shared.ptr, { flags, shared, props });
}
// ... boilerplate methods
}
기본 프록시 핸들러 대신 시퀀스에 다음 핸들러를 사용합니다. 사용하지 않습니다.
const PyProxySequenceHandlers = {
isExtensible() {
return true;
},
has(jsobj, jskey) {
if (typeof jskey === "string" && /^[0-9]+$/.test(jskey)) {
// Note: if the number was negative it didn't match the pattern
return Number(jskey) < jsobj.length;
}
return PyProxyHandlers.has(jsobj, jskey);
},
get(jsobj, jskey) {
if (jskey === "length") {
return jsobj.length;
}
if (typeof jskey === "string" && /^[0-9]+$/.test(jskey)) {
try {
return PyProxyGetItemMixin.prototype.get.call(jsobj, Number(jskey));
} catch (e) {
if (isPythonError(e) && e.type == "IndexError") {
return undefined;
}
throw e;
}
}
return PyProxyHandlers.get(jsobj, jskey);
},
set(jsobj: PyProxy, jskey: any, jsval: any): boolean {
if (typeof jskey === "string" && /^[0-9]+$/.test(jskey)) {
try {
PyProxySetItemMixin.prototype.set.call(jsobj, Number(jskey), jsval);
return true;
} catch (e) {
if (isPythonError(e) && e.type == "IndexError") {
return false;
}
throw e;
}
}
return PyProxyHandlers.set(jsobj, jskey, jsval);
},
deleteProperty(jsobj: PyProxy, jskey: any): boolean {
if (typeof jskey === "string" && /^[0-9]+$/.test(jskey)) {
try {
PyProxySetItemMixin.prototype.delete.call(jsobj, Number(jskey));
return true;
} catch (e) {
if (isPythonError(e) && e.type == "IndexError") {
return false;
}
throw e;
}
}
return PyProxyHandlers.deleteProperty(jsobj, jskey);
},
ownKeys(jsobj: PyProxy): (string | symbol)[] {
const result = PyProxyHandlers.ownKeys(jsobj);
result.push(
...Array.from({ length: jsobj.length }, (_, k) => k.toString()),
);
result.push("length");
return result;
},
};
IS_MUTABLE_SEQUENCE 믹스인
시퀀스를 변경하는 몇 가지 추가 Array 메서드를 추가합니다.
class PyMutableSequenceMixin {
reverse() {
// Same as the Python reverse method except it returns this instead of undefined
this.$reverse();
return this;
}
push(...elts: any[]) {
for (const elt of elts) {
this.append(elt);
}
return this.length;
}
splice(start, deleteCount, ...items) {
if (deleteCount === undefined) {
// Max signed size
deleteCount = (1 << 31) - 1;
}
let stop = start + deleteCount;
if (stop > this.length) {
stop = this.length;
}
const pythonSplice = makePythonFunction(`
def splice(array, start, stop, items):
from jstypes.ffi import to_js
result = to_js(array[start:stop], depth=1)
array[start:stop] = items
return result
`);
return pythonSplice(this, start, stop, items);
}
pop() {
const pythonPop = makePythonFunction(`
def pop(array):
return array.pop()
`);
return pythonPop(this);
}
shift() {
const pythonShift = makePythonFunction(`
def pop(array):
return array.pop(0)
`);
return pythonShift(this);
}
unshift(...elts) {
elts.forEach((elt, idx) => {
this.insert(idx, elt);
});
return this.length;
}
// Boilerplate methods
copyWithin(...args): any {
Array.prototype.copyWithin.apply(this, args);
return this;
}
fill(...args) {
Array.prototype.fill.apply(this, args);
return this;
}
}
IS_JS_JSON_DICT 믹스인
IS_JS_JSON_DICT 플래그에만 해당하는 메서드는 없지만, 다음 프록시 핸들러를 사용합니다. 두 가지 예외를 제외하면 속성을 딕셔너리의 항목으로 조회하는 것을 선호합니다.
- 심볼은 항상
PyProxy자체에서 조회합니다. - 또한
PyProxy에서$$flags,copy(),constructor,destroy및toString이라는 키를 조회합니다.
모든 Python 딕셔너리 메서드는 같은 이름의 키에 가려집니다.
const PyProxyJsJsonDictHandlers = {
isExtensible(): boolean {
return true;
},
has(jsobj: PyProxy, jskey: string | symbol): boolean {
if (PyContainsMixin.prototype.has.call(jsobj, jskey)) {
return true;
}
// If it doesn't exist as a string key and it looks like a number,
// try again with the number
if (typeof jskey === "string" && /^-?[0-9]+$/.test(jskey)) {
return PyContainsMixin.prototype.has.call(jsobj, Number(jskey));
}
return false;
},
get(jsobj, jskey): any {
if (
typeof jskey === "symbol" ||
["$$flags", "copy", "constructor", "destroy", "toString"].includes(jskey)
) {
return Reflect.get(...arguments);
}
const result = PyProxyGetItemMixin.prototype.get.call(jsobj, jskey);
if (
result !== undefined ||
PyContainsMixin.prototype.has.call(jsobj, jskey)
) {
return result;
}
if (typeof jskey === "string" && /^-?[0-9]+$/.test(jskey)) {
return PyProxyGetItemMixin.prototype.get.call(jsobj, Number(jskey));
}
return Reflect.get(...arguments);
},
set(jsobj, jskey, jsval): boolean {
if (typeof jskey === "symbol") {
return false;
}
if (
!PyContainsMixin.prototype.has.call(jsobj, jskey) &&
typeof jskey === "string" &&
/^-?[0-9]+$/.test(jskey)
) {
jskey = Number(jskey);
}
try {
PyProxySetItemMixin.prototype.set.call(jsobj, jskey, jsval);
return true;
} catch (e) {
if (isPythonError(e) && e.type === "KeyError") {
return false;
}
throw e;
}
},
deleteProperty(jsobj: PyProxy, jskey: string | symbol | number): boolean {
if (typeof jskey === "symbol") {
return false;
}
if (
!PyContainsMixin.prototype.has.call(jsobj, jskey) &&
typeof jskey === "string" &&
/^-?[0-9]+$/.test(jskey)
) {
jskey = Number(jskey);
}
try {
PyProxySetItemMixin.prototype.delete.call(jsobj, jskey);
return true;
} catch (e) {
if (isPythonError(e) && e.type === "KeyError") {
return false;
}
throw e;
}
},
getOwnPropertyDescriptor(jsobj: PyProxy, prop: any) {
if (!PyProxyJsJsonDictHandlers.has(jsobj, prop)) {
return undefined;
}
const value = PyProxyJsJsonDictHandlers.get(jsobj, prop);
return {
configurable: true,
enumerable: true,
value,
writable: true,
};
},
ownKeys(jsobj: PyProxy): (string | symbol)[] {
const pythonDictOwnKeys = makePythonFunction(`
def dict_own_keys(d):
from jstypes.ffi import to_js
result = set()
for key in d:
if isinstance(key, str):
result.add(key)
elif isinstance(key, (int, float)):
result.add(str(key))
return to_js(result)
`);
return pythonDictOwnKeys(jsobj);
},
};
IS_JS_JSON_SEQUENCE 믹스인
이는 프록시의 프로토타입이나 핸들러에 직접적인 영향을 주지 않습니다. 그러나 목록을 인덱싱하거나 목록을 반복할 때 결과에 asJsJson()을 적용합니다.
심층 변환
JavaScript에서 Python으로의 심층 변환을 수행하도록 JSProxy.to_py()를 정의하고, Python에서 JavaScript로의 심층 변환을 수행하도록 jstypes.ffi.to_js()를 정의합니다. 이러한 함수가 서로의 역함수가 되도록 의도된 것은 아니라는 점에 유의하십시오.
JavaScript에서 Python으로
JSProxy.to_py() 메서드는 다음과 같이 변환합니다.
Array==>listMap==>dictSet==>setObject==>dict로 변환하지만,constructor가Object이거나undefined인 경우에만 해당합니다. 그 밖의 객체는 그대로 둡니다.
다음과 같은 선택적 인자를 사용합니다.
depth- 정수이며, 변환할 최대 깊이를 지정합니다. 예를 들어
depth=1로 설정하면 정확히 한 수준만 변환할 수 있습니다. default_converter- 객체에 대해 알려진 변환 방법이 없을 때 호출할 함수입니다.
기본 변환기는 세 개의 인자를 사용합니다.
jsobj- 변환할 객체입니다.
convert- 재귀 호출을 허용합니다.
cache_conversion- 객체의 변환 결과를 캐시하여 자기 참조 데이터를 변환할 수 있도록 합니다.
예를 들어 JavaScript의 Pair 클래스가 있고 이를 리스트로 변환하려는 경우, 다음과 같은 default_converter를 사용할 수 있습니다.
def pair_converter(jsobj, convert, cache_conversion):
if jsobj.constructor.name != "Pair":
return jsobj
result = []
cache_conversion(jsobj, result)
result.append(convert(jsobj.first))
result.append(convert(jsobj.second))
return result
convert를 재귀적으로 호출하기 전에 먼저 결과를 캐시하면, jsobj.first가 jsobj를 간접적으로 참조하더라도 이를 올바르게 변환할 수 있습니다.
to_py 메서드의 전체 의사 코드는 다음과 같습니다.
def to_py(jsobj, *, depth=-1, default_converter=None):
cache = {}
return ToPyConverter(depth, default_converter).convert(jsobj)
class ToPyConverter:
def __init__(self, depth, default_converter):
self.cache = {}
self.depth = depth
self.default_converter = default_converter
def cache_conversion(self, jsobj, pyobj):
self.cache[jsobj.js_id] = pyobj
def convert(self, jsobj):
if self.depth == 0 or not isinstance(jsobj, JSProxy):
return jsobj
if result := self.cache.get(jsobj.js_id):
return result
from jstypes.global_this import Array, Object
type_tag = getTypeTag(jsobj)
self.depth -= 1
try:
if Array.isArray(jsobj):
return self.convert_list(jsobj)
if type_tag == "[object Map]":
return self.convert_map(jsobj, jsobj.entries())
if type_tag == "[object Set]":
return self.convert_set(jsobj)
if type_tag == "[object Object]" and (jsobj.constructor in [None, Object]):
return self.convert_map(jsobj, Object.entries(jsobj))
if self.default_converter is not None:
return self.default_converter(jsobj, self.convert, self.cache_conversion)
return jsobj
finally:
self.depth += 1
def convert_list(self, jsobj):
result = []
self.cache_conversion(jsobj, result)
for item in jsobj:
result.append(self.convert(item))
return result
def convert_map(self, jsobj, entries):
result = {}
self.cache_conversion(jsobj, result)
for [key, val] in entries:
result[key] = self.convert(val)
return result
def convert_set(self, jsobj):
result = set()
self.cache_conversion(jsobj, result)
for key in jsobj:
result.add(self.convert(key))
return result
Python에서 JavaScript로
def to_js(
obj,
/,
*,
depth=-1,
pyproxies=None,
create_pyproxies=True,
dict_converter=None,
default_converter=None,
eager_converter=None,
):
converter = ToJsConverter(
depth,
pyproxies,
create_pyproxies,
dict_converter,
default_converter,
eager_converter,
)
result = converter.convert(obj)
converter.postprocess()
return result
class ToJsConverter:
def __init__(
self,
depth,
pyproxies,
create_pyproxies,
dict_converter,
default_converter,
eager_converter,
):
self.depth = depth
self.pyproxies = pyproxies
self.create_pyproxies = create_pyproxies
if dict_converter is None:
dict_converter = Object.fromEntries
self.dict_converter = dict_converter
self.default_converter = default_converter
self.eager_converter = eager_converter
self.cache = {}
self.post_process_list = []
self.pairs_to_dict_map = {}
def cache_conversion(self, pyobj, jsobj):
self.cache[id(pyobj)] = jsobj
def postprocess(self):
# Replace any NoValue's that appear once we've certainly computed
# their correct conversions
for parent, key, pyobj_id in self.post_process_list:
real_value = self.cache[pyobj_id]
# If it was a dictionary, we need to lookup the actual result object
real_parent = self.pairs_to_dict_map.get(parent.js_id, parent)
real_parent[key] = real_value
@contextmanager
def decrement_depth(self):
self.depth -= 1
try:
yield
finally:
self.depth += 1
def convert(self, pyobj):
if self.depth == 0 or isinstance(pyobj, JSProxy):
return pyobj
if result := self.cache.get(id(pyobj)):
return result
with self.decrement_depth():
if self.eager_converter:
return self.eager_converter(
pyobj, self.convert_no_eager_public, self.cache_conversion
)
return self.convert_no_eager(pyobj)
def convert_no_eager_public(self, pyobj):
with self.decrement_depth():
return self.convert_no_eager(pyobj)
def convert_no_eager(self, pyobj):
if isinstance(pyobj, (tuple, list)):
return self.convert_sequence(pyobj)
if isinstance(pyobj, dict):
return self.convert_dict(pyobj)
if isinstance(pyobj, set):
return self.convert_set(pyobj)
if self.default_converter:
return self.default_converter(
pyobj, self.convert_no_eager_public, self.cache_conversion
)
if not self.create_pyproxies:
raise ConversionError(
f"No conversion available for {pyobj!r} and create_pyproxies=False passed"
)
result = create_proxy(pyobj)
if self.pyproxies is not None:
self.pyproxies.append(result)
return result
def convert_sequence(self, pyobj):
from jstypes.global_this import Array
result = Array.new()
self.cache_conversion(pyobj, result)
for idx, val in enumerate(pyobj):
converted = self.convert(val)
if converted is NoValue:
self.post_process_list.append((result, idx, id(val)))
result.push(converted)
return result
def convert_dict(self, pyobj):
from jstypes.global_this import Array
# Temporarily store NoValue in the cache since we only get the
# actual value from dict_converter. We'll replace these with the
# correct values in the postprocess step
self.cache_conversion(pyobj, NoValue)
pairs = Array.new()
for [key, value] in pyobj.items():
converted = self.convert(value)
if converted is NoValue:
self.post_process_list.append((pairs, key, id(value)))
pairs.push(Array.new(key, converted))
result = self.dict_converter(pairs)
self.pairs_to_dict_map[pairs.js_id] = result
# Update the cache to point to the actual result
self.cache_conversion(pyobj, result)
return result
def convert_set(self, pyobj):
from jstypes.global_this import Set
result = Set.new()
self.cache_conversion(pyobj, result)
for key in pyobj:
if isinstance(key, JSProxy):
raise ConversionError(
f"Cannot use {key!r} as a key for a JavaScript Set"
)
result.add(key)
return result
jstypes.global_this 모듈
jstypes.global_this 모듈을 사용하면 JavaScript에서 객체를 가져올 수 있습니다. 정의는 다음과 같습니다.
import sys
from jstypes.code import run_js
from jstypes.ffi import JSProxy
from importlib.abc import Loader, MetaPathFinder
from importlib.util import spec_from_loader
class JSLoader(Loader):
def __init__(self, jsproxy):
self.jsproxy = jsproxy
def create_module(self, spec):
return self.jsproxy
def exec_module(self, module):
pass
def is_package(self, fullname):
return True
class JSFinder(MetaPathFinder):
def _get_object(self, fullname):
[parent, _, child] = fullname.rpartition(".")
if not parent:
if child == "jstypes":
return run_js("globalThis")
return None
parent_module = sys.modules[parent]
if not isinstance(parent_module, JSProxy):
# Not one of us.
return None
jsproxy = getattr(parent_module, child, None)
if not isinstance(jsproxy, JSProxy):
raise ModuleNotFoundError(f"No module named {fullname!r}", name=fullname)
return jsproxy
def find_spec(
self,
fullname,
path,
target,
):
jsproxy = self._get_object(fullname)
loader = JSLoader(jsproxy)
return spec_from_loader(fullname, loader, origin="javascript")
finder = JSFinder()
sys.meta_path.insert(0, finder)
del sys.modules["jstypes.global_this"]
import jstypes.global_this
sys.meta_path.remove(finder)
sys.meta_path.append(finder)
jstypes 패키지
이 패키지에는 비어 있는 __init__.py와 두 개의 하위 모듈이 있습니다.
jstypes.ffi 모듈
다음과 같은 속성이 있습니다:
create_proxy(x): 이는 create_jsproxy(createPyProxy(x))를 반환합니다.
jsnull: JavaScript null 값으로 변환되거나 그 값에서 변환되는 특수 값입니다.
JSNull: jsnull의 타입입니다.
def destroy_proxies(proxies):
for proxy in proxies:
proxy.destroy()
to_js: 심층 변환 섹션의 정의를 참조하십시오.
JSArray: 이는 type(run_js("[]"))입니다.
JSCallable: 이는 type(run_js("() => {}"))입니다.
JSDoubleProxy: 이는 type(create_proxy({}))입니다.
JSException: 이는 type(run_js("new Error()"))입니다.
JSGenerator: 이는 type(run_js("(function*(){})()"))입니다.
JSIterable: 이는 type(run_js("({[Symbol.iterator](){}})"))입니다.
JSIterator: 이는 type(run_js("({next(){}})"))입니다.
JSMap: 이는 type(run_js("({get(){}})"))입니다.
JSMutableMap: 이는 type(run_js("new Map()"))입니다.
JSProxy: 이는 type(run_js("({})"))입니다
JSBigInt: 다음과 같이 정의됩니다:
def _int_to_bigint(x):
if isinstance(x, int):
return JSBigInt(x)
return x
class JSBigInt(int):
# unary ops
def __abs__(self):
return JSBigInt(int.__abs__(self))
def __invert__(self):
return JSBigInt(int.__invert__(self))
def __neg__(self):
return JSBigInt(int.__neg__(self))
def __pos__(self):
return JSBigInt(int.__pos__(self))
# binary ops
def __add__(self, other):
return _int_to_bigint(int.__add__(self, other))
def __and__(self, other):
return _int_to_bigint(int.__and__(self, other))
def __floordiv__(self, other):
return _int_to_bigint(int.__floordiv__(self, other))
def __lshift__(self, other):
return _int_to_bigint(int.__lshift__(self, other))
def __mod__(self, other):
return _int_to_bigint(int.__mod__(self, other))
def __or__(self, other):
return _int_to_bigint(int.__or__(self, other))
def __pow__(self, other, modulus = None):
return _int_to_bigint(int.__pow__(self, other, modulus))
def __rshift__(self, other):
return _int_to_bigint(int.__rshift__(self, other))
def __sub__(self, other):
return _int_to_bigint(int.__sub__(self, other))
def __xor__(self, other):
return _int_to_bigint(int.__xor__(self, other))
jstypes.code 모듈
이는 run_js 함수를 노출합니다.
json 모듈의 변경 사항
json 모듈은 jsnull을 null로 직렬화하도록 업데이트됩니다.
하위 호환성
새 API만 추가합니다. Pyodide의 하위 호환성에 관한 우려가 있습니다. Pyodide와 비교하여 여러 모듈과 타입의 이름을 변경했습니다:
pyodide패키지가jstypes로 변경됩니다.js모듈이jstypes.global_this로 변경됩니다.- 모든
JSProxy변형은JSProxy처럼 대문자로 표기됩니다.
다음 릴리스에서 Pyodide는 변경된 이름과 원래 이름을 모두 지원합니다. 또한 이전 이름에 대한 하위 호환성 심을 포함하는 패키지를 PyPI에 업로드합니다.
보안 관련 사항
Python이 실행될 수 있는 완전한 샌드박스 플랫폼 중 몇 안 되는 플랫폼에 대한 지원이 향상됩니다.
이 내용을 가르치는 방법
참조 구현
감사의 말
Mike Droettboom, Roman Yurchak, Gyeongjae Choi, Andrea Giammarchi
Copyright
This document is placed in the public domain or under the CC0-1.0-Universal license, whichever is more permissive.