코딩 가이드

이 문서는 PyPy 코드 베이스로 작업할 때 지켜야 할 코딩 요구 사항과 관례를 설명합니다. 주의 깊게 읽어 주시고, 궁금한 점이 있으면 언제든지 질문해 주십시오. 이 문서는 코딩 스타일 문제는 그다지 많이 다루지 않습니다. 다만 대체로 PEP 8을 따릅니다. 확실하지 않을 때는 코드 베이스에 이미 있는 스타일을 따르십시오.

개요 및 동기

저희는 언어로서 알고리즘상의 문제에서 한발 물러설 수 있게 해 주는 Python의 잘 알려진 능력을 활용하여, Python으로 Python 인터프리터를 작성하고 있습니다. 언뜻 보기에는 이것이 인터프리터의 동작 방식을 더 잘 이해하게 해 주는 것 외에는 아무것도 이루지 못한다고 생각할 수도 있습니다. 그것만으로도 충분히 해 볼 만한 가치가 있지만, 저희에게는 훨씬 더 큰 목표가 있습니다.

CPython 대 PyPy

CPython 구현과 비교하면, Python이 C 코드의 역할을 맡습니다. 우리는 CPython 인터프리터를 Python 자체로 다시 작성합니다. C 수준에서 더 유연한 인터프리터를 작성하는 것을 목표로 삼을 수도 있지만, 우리는 Python을 사용하여 인터프리터를 또 다른 방식으로 기술하고자 합니다.

분명한 장점은 이러한 기술이 더 짧고 읽기 쉬우며, 많은 구현 세부 사항이 사라진다는 것입니다. 이 접근 방식의 단점은 이 인터프리터가 CPython 위에서 실행되는 한 견딜 수 없을 만큼 느리다는 것입니다.

다시 쓸모 있는 인터프리터를 얻으려면 Python에 대한 고수준 기술을 더 저수준의 기술로 변환해야 합니다. 꽤 간단한 방법 하나는 PyPy 인터프리터에 대해 전체 프로그램 분석(whole program analysis)을 수행하여 다시 C 소스를 생성하는 것입니다. 다른 방법도 많이 있지만, 여기서는 다소 정석적인 이 접근 방식을 따릅시다.

애플리케이션 수준과 인터프리터 수준의 실행과 객체

코드베이스 전체를 Python으로 구현하므로 반드시 알아 두어야 할 중요한 구분이 있습니다. 바로 인터프리터 수준(interpreter-level) 객체와 애플리케이션 수준(application-level) 객체 사이의 구분입니다. 후자는 일반적인 Python 프로그램을 작성할 때 다루는 객체입니다. 그러나 인터프리터 수준 코드는 애플리케이션 수준 객체의 연산을 호출할 수도, 속성에 접근할 수도 없습니다. PyPy에서 인터프리터 수준 코드는 바로 알아볼 수 있습니다. 변수와 객체 이름의 절반이 w_로 시작하기 때문이며, 이는 해당 값이 wrapped 애플리케이션 수준 값임을 나타냅니다.

간단한 예제로 그 차이를 살펴봅시다. 두 변수 a와 b의 내용을 더하려면 애플리케이션 수준에서는 간단히 a+b라고 작성합니다. 이에 비해 이와 동등한 인터프리터 수준의 코드는 space.add(w_a, w_b)입니다. 여기서 space는 객체 공간(object space)의 인스턴스이며, w_a와 w_b는 두 변수를 래핑한 버전에 흔히 붙이는 이름입니다.

CPython이 같은 문제를 어떻게 다루는지 떠올려 보면 도움이 됩니다. CPython에서 인터프리터 수준 코드는 C로 작성되므로, 덧셈을 수행하는 전형적인 코드는 PyNumber_Add(p_a, p_b)이며, 여기서 p_a와 p_b는 PyObject* 타입의 C 변수입니다. 이는 우리가 인터프리터 수준 코드를 Python으로 작성하는 방식과 개념적으로 비슷합니다.

또한 PyPy에서는 인터프리터 수준의 예외와 애플리케이션 수준의 예외를 명확히 구분해야 합니다. 애플리케이션 예외는 항상 OperationError 인스턴스 안에 담겨 있습니다. 이렇게 하면 인터프리터 수준 코드에서 발생하는 실패(또는 버그)와 우리가 해석하고 있는 Python 애플리케이션 수준 프로그램에서 나타나는 실패를 쉽게 구별할 수 있습니다.

애플리케이션 수준이 더 바람직한 경우가 많습니다

애플리케이션 수준 코드는 훨씬 더 고수준이므로, 그만큼 작성하고 디버깅하기도 더 쉽습니다. 예를 들어, dict 객체의 update 메서드를 구현하려 한다고 가정해 봅시다. 애플리케이션 수준에서 프로그래밍하면 명백하고 단순한 구현, 즉 update의 실행 가능한 정의처럼 보이는 구현을 작성할 수 있습니다. 예를 들면 다음과 같습니다.:

def update(self, other):
    for k in other.keys():
        self[k] = other[k]

인터프리터 수준에서만 코드를 작성해야 한다면, 훨씬 더 저수준이고 복잡한 코드를 작성해야 할 것입니다. 예를 들면 다음과 같습니다:

def update(space, w_self, w_other):
    w_keys = space.call_method(w_other, 'keys')
    w_iter = space.iter(w_keys)
    while True:
        try:
            w_key = space.next(w_iter)
        except OperationError as e:
            if not e.match(space, space.w_StopIteration):
                raise       # re-raise other app-level exceptions
            break
        w_value = space.getitem(w_other, w_key)
        space.setitem(w_self, w_key, w_value)

이 인터프리터 수준의 구현은 C 소스 코드와 훨씬 더 비슷해 보입니다. 그래도 메모리 관리 세부 사항을 포함하지 않고 Python 고유의 예외 메커니즘을 사용할 수 있기 때문에, C로 작성된 대응 코드보다는 여전히 읽기 쉽습니다.

어쨌든 애플리케이션 수준 구현이 인터프리터 수준 구현보다 확실히 더 읽기 쉽고, 더 우아하며, 유지 보수하기도 더 쉽다는 점은 분명합니다(실제로 PyPy에서 dict.update는 애플리케이션 수준에서 구현되어 있습니다).

실제로 PyPy의 거의 모든 부분에서 인터프리터 수준 코드 사이사이에 애플리케이션 수준 코드가 섞여 있는 것을 볼 수 있습니다. 몇 가지 부트스트래핑 문제(애플리케이션 수준 함수는 실행되기 전에 객체 공간(object space)이 어느 정도 초기화되어 있어야 합니다)를 제외하면, 대개는 애플리케이션 수준 코드가 더 바람직합니다. 저희에게는 (‘Gateway’라고 부르는) 추상화가 있어서, 함수의 호출자는 특정 함수가 애플리케이션 수준에서 구현되었는지 인터프리터 수준에서 구현되었는지 알 필요가 없습니다.

저희 런타임 인터프리터는 “RPython”입니다

C 코드 생성기를 구현할 수 있도록 인터프리터 수준의 모든 코드는 Python 언어의 부분 집합만 사용해야 하며, 저수준 언어로의 변환을 가능하게 하는 몇 가지 규칙을 따릅니다. 애플리케이션 수준의 코드는 여전히 Python의 표현력을 모두 활용할 수 있습니다.

소스 간 변환(예: Starkiller나 비교적 최근의 ShedSkin)과 달리, 우리는 Python 인터프리터를 이루는 실행 중인 Python 코드 객체에서 변환을 시작합니다. 바이트코드를 해석하는 작업을 수행할 때 우리 Python 구현은 흔히 “RPythonic”이라고 부르는 정적인 방식으로 동작해야 합니다.

그러나 PyPy 인터프리터를 Python 프로그램으로 시작하면, 특정 시점에 도달하기 전까지는 Python 언어의 모든 기능을 사용할 수 있지만, 그 시점부터 실행되는 모든 것은 정적이어야 합니다. 즉, 초기화 중에는 우리 프로그램이 동적 코드 생성을 포함하여 Python의 동적 특성을 모두 자유롭게 사용할 수 있습니다.

현재 구현에서 꽤 우아한 예를 찾아볼 수 있습니다. Python 인터프리터의 모든 옵코드를 정의하기 위해 dis 모듈을 임포트하고, 이를 사용하여 우리 바이트코드 인터프리터를 초기화합니다. (pypy/interpreter/pyopcode.py의 __initclass__를 참고하십시오.) 이렇게 하면 PyPy에 별도의 모듈을 추가하지 않아도 됩니다. 임포트 코드는 시작 시점에 실행되며, 이때는 CPython의 내장 import 함수를 사용할 수 있습니다.

시작 코드의 실행이 끝나면, 그 결과로 만들어진 모든 객체, 함수, 코드 블록 등은 아래에서 설명할 특정 런타임 제약을 지켜야 합니다. 그 이유에 대한 배경은 다음과 같습니다. 변환 과정에서 전체 프로그램 분석(“타입 추론”)이 수행되는데, 이 분석은 RPython에 정의된 제약을 활용합니다. 이를 통해 코드 생성기는 예를 들어 순수 정수 객체 인스턴스를 효율적인 기계 수준의 대체물로 바꾸어 생성할 수 있습니다.

줄 바꿈 규칙

래핑(Wrapping)

PyPy는 두 가지 수준의 Python 소스 코드로 이루어져 있습니다. 한편으로는 일반적인 Python 코드처럼 보이며 Python 코드에서 기대할 법한 방식으로 일부 기능을 구현하는 애플리케이션 수준 코드가 있습니다(예를 들어 zip() 같은 일부 내장 함수는 순수 Python 구현으로 제공할 수 있습니다). 또한 인터프리터의 데이터와 객체를 더 직접적으로 조작해야 하는 기능을 위한 인터프리터 수준 코드도 있습니다(예를 들어 인터프리터의 메인 루프와 여러 객체 공간(object space) 등).

애플리케이션 수준 코드는 객체 공간(object space)을 명시적으로 보지 않습니다. 애플리케이션 수준 코드는 자신이 다루는 객체를 지원하기 위해 객체 공간을 사용하여 실행되지만, 이는 암묵적으로 이루어집니다. 애플리케이션 수준 코드에는 특별한 규칙이 필요하지 않습니다. 이어지는 내용은 인터프리터 수준 코드만 다룹니다. (혼동을 피하려면, 이상적으로는 어떤 애플리케이션 수준 변수도 space나 w_xxx라는 이름을 사용하지 않아야 합니다.)

위 예제에서 아낌없이 쓰인 w_ 접두사는 PyPy 코딩 관례상 래핑된(또는 박싱된) 객체를 다루고 있음을 나타냅니다. 즉, 대응하는 애플리케이션 수준 객체를 구현하기 위해 객체 공간(object space)이 만들어 내는 인터프리터 수준 객체를 다룬다는 뜻입니다. 각 객체 공간(object space)은 단순한 내장 타입의 객체를 두 수준 사이에서 옮기는 wrap, unwrap, int_w, interpclass_w 등의 연산을 제공합니다. 또한 각 객체 공간(object space)은 다른 Python 타입을 어느 정도의 내부 구조를 갖춘 적절한 인터프리터 수준 클래스로 구현합니다.

예를 들어, 애플리케이션 수준의 Python list는 표준 객체 공간(object space)에 의해 W_ListObject의 인스턴스로 구현되며, 이 인스턴스에는 wrappeditems라는 인스턴스 속성(애플리케이션 수준 리스트의 항목들을 래핑된 객체로 담고 있는 인터프리터 수준 리스트)이 있습니다.

규칙은 아래에서 더 자세히 설명합니다.

명명 규칙

  • space: 객체 공간(object space)은 인터프리터 수준 코드에서만 보이며, 그곳에서는 관례상 space라는 이름으로 전달됩니다.
  • w_xxx: 애플리케이션 수준 코드에서 보이는 모든 객체는 객체 공간(object space)이 명시적으로 관리하는 객체입니다. 인터프리터 수준의 관점에서는 이를 래핑된(wrapped) 객체라고 부릅니다. w_ 접두사는 모든 종류의 애플리케이션 수준 객체에 사용됩니다.
  • xxx_w: 래핑된 객체를 담는 인터프리터 수준의 컨테이너입니다. 예를 들어 래핑된 객체를 담고 있는 리스트나 딕셔너리가 이에 해당합니다. 리스트나 딕셔너리인 래핑된 객체와 혼동하지 마십시오. 그러한 객체는 일반적인 래핑된 객체이므로 w_ 접두사를 사용합니다.

w_xxx에 대한 연산

핵심 바이트코드 인터프리터는 래핑된 객체를 블랙박스로 취급합니다. 래핑된 객체를 직접 들여다보는 것은 허용되지 않습니다. 허용되는 연산은 모두 객체 공간(object space)에 구현되어 있습니다. 이 연산들은 space.xxx() 형태의 이름을 가지며, 여기서 xxx는 표준 연산 이름(add, getattr, call, eq…)입니다. 이 연산들은 객체 공간 문서에 설명되어 있습니다.

짧게 경고하겠습니다. w_x == w_y나 w_x is w_y를 사용하지 마십시오! 두 래퍼가 애플리케이션 수준에서 같은 객체로 보이는 것을 담고 있더라도, 두 래퍼가 어떤 식으로든 서로 관련되어 있다고 볼 근거가 없기 때문입니다. 동등성을 확인하려면 space.is_true(space.eq(w_x, w_y))를 사용하십시오. 인터프리터 수준의 bool을 바로 반환하는 단축 함수 space.eq_w(w_x, w_y)를 사용하면 더 좋습니다. 동일성을 확인하려면 space.is_true(space.is_(w_x, w_y))를 사용하십시오. space.is_w(w_x, w_y)를 사용하면 더 좋습니다.

애플리케이션 수준 예외

인터프리터 수준 코드에서는 예외를 자유롭게 사용할 수 있습니다. 그러나 애플리케이션 수준의 모든 예외는 인터프리터 수준에서 OperationError로 표현됩니다. 다시 말해, 애플리케이션 수준에서 드러날 수 있는 모든 예외는 내부적으로 OperationError입니다. 객체 공간(object space) 연산(space.add() 등)이 보고하는 모든 오류가 이에 해당합니다.

애플리케이션 수준 예외를 발생시키려면:

from pypy.interpreter.error import oefmt

raise oefmt(space.w_XxxError, "message")

raise oefmt(space.w_XxxError, "file '%s' not found in '%s'", filename, dir)

raise oefmt(space.w_XxxError, "file descriptor '%d' not open", fd)

특정 애플리케이션 수준 예외를 잡으려면:

try:
    ...
except OperationError as e:
    if not e.match(space, space.w_XxxError):
        raise
    ...

이 구문은 애플리케이션 수준의 모든 예외를 잡으므로, 관심 있는 특정 w_XxxError와 대조하고 다른 예외는 다시 발생시켜야 합니다. 예외 인스턴스 e에는 확인할 수 있는 두 가지 속성 e.w_type과 e.w_value가 있습니다. 예외를 대조할 때 e.w_type을 사용하지 마십시오. 그렇게 하면 서브클래스의 인스턴스인 예외를 놓치게 됩니다.

PyPy의 모듈

애플리케이션 프로그램에서 보이는 모듈은 인터프리터 수준 또는 애플리케이션 수준 파일에서 임포트됩니다. PyPy는 CPython 표준 라이브러리의 파이썬 모듈을 거의 모두 재사용하며, 현재는 버전 2.7.8의 모듈을 사용합니다. 이들은 CPython의 구현 세부 사항에 의존하기 때문에 때때로 modify modules가 필요하며, 회귀 테스트는 그보다 더 자주 수정해야 합니다.

원본 CPython 모듈을 단순히 수정하는 것이 아니라 처음부터 다시 작성해야 하는 경우에는, 해당 모듈을 순수 애플리케이션 수준 모듈로 lib_pypy/에 넣습니다.

인터프리터 수준 객체에 접근해야 할 때는 모듈을 pypy/module에 둡니다. 이러한 모듈은 mixed module mechanism을 사용하므로, 인터프리터 수준 부분과 애플리케이션 수준 부분을 모두 활용하여 편리하게 구현할 수 있습니다. 순수하게 인터프리터 수준으로만 이루어진 모듈을 위한 별도의 기능은 없다는 점에 유의하십시오. 혼합 모듈(mixed module)을 작성하고 애플리케이션 수준 부분을 비워 두기만 하면 됩니다.

모듈 구현 위치 확인하기

py.py를 실행할 때 모듈이 어디에서 오는지 대화형으로 확인할 수 있습니다. 다음은 모듈이 있을 수 있는 위치의 예입니다.:

>>>> import sys
>>>> sys.__file__
'/home/hpk/pypy-dist/pypy/module/sys'

>>>> import cPickle
>>>> cPickle.__file__
'/home/hpk/pypy-dist/lib_pypy/cPickle..py'

>>>> import os
>>>> os.__file__
'/home/hpk/pypy-dist/lib-python/2.7/os.py'
>>>>

모듈 디렉터리 / 임포트 순서

PyPy가 Python 모듈을 찾는 순서는 다음과 같습니다:

pypy/module (모듈)

sys 모듈과 __builtin__ 모듈처럼 인터프리터 수준과 애플리케이션 수준이 혼합된 내장 모듈.

PYTHONPATH의 내용

PYTHONPATH 환경 변수에 지정된 :로 구분된 디렉터리 목록의 각 디렉터리에서 애플리케이션 수준 모듈을 찾습니다.

lib_pypy/

모듈을 순수 Python으로 다시 구현한 코드가 들어 있습니다.

lib-python/2.7/

수정된 CPython 라이브러리입니다.

CPython 라이브러리 모듈 또는 회귀 테스트 수정하기

PyPy는 CPython과의 호환성이 매우 높지만, 저희가 보유한 표준 라이브러리 사본에 포함된 모듈을 수정해야 할 때가 있습니다. 이는 대개 PyPy가 기본적으로 모든 클래스를 new-style 클래스로 다루는 반면, CPython에는 일부 클래스가 old-style 클래스라는 점에 의존하는 부분이 여러 곳 있기 때문입니다.

저희는 이러한 변경 사항을 원래 위치에서 그대로 유지 관리합니다. 무엇이 변경되었는지 확인할 수 있도록, 수정되지 않은 CPython 표준 라이브러리(stdlib)를 담고 있는 vendor/stdlib이라는 브랜치를 두고 있습니다.

인터프리터/애플리케이션 수준 혼합 모듈 구현하기

모듈이 PyPy 인터프리터 수준에 접근해야 한다면, 그 모듈은 혼합 모듈(mixed module)로 구현됩니다.

혼합 모듈(mixed module)은 pypy/module에 있는 디렉터리이며, 모듈의 각 이름이 어디에서 오는지 지정하는 __init__.py 파일을 포함합니다. 지정된 이름만 혼합 모듈의 애플리케이션 레벨(applevel) 네임스페이스로 내보내집니다.

때로는 일부 함수를 C(또는 다른 대상 언어)로 직접 작성해야 할 때가 있습니다. 자세한 내용은 rffi를 참조하십시오.

애플리케이션 수준 정의

애플리케이션 수준 명세는 pypy/module에 있는 디렉터리의 __init__.py 파일에 들어 있는 appleveldefs 딕셔너리에서 찾을 수 있습니다. 예를 들어, pypy/module/__builtin__/__init__.py에서 __builtin__.locals가 어디에서 오는지 지정하는 다음 항목을 찾을 수 있습니다:

...
'locals'        : 'app_inspect.locals',
...

app_ 접두사는 하위 모듈 app_inspect가 애플리케이션 수준에서 해석된다는 것을 나타내며, 이에 따라 locals에 대한 래핑된 함수 값이 추출됩니다.

인터프리터 수준 정의

인터프리터 수준 명세는 pypy/module 아래 디렉터리들의 __init__.py 파일에 있는 interpleveldefs 딕셔너리에서 찾을 수 있습니다. 예를 들어, pypy/module/__builtin__/__init__.py에서는 다음 항목이 __builtin__.len이 어디에서 오는지 지정합니다.:

...
'len'       : 'operation.len',
...

operation 하위 모듈은 인터프리터 수준에 있으며, len은 애플리케이션 수준에 노출될 수 있어야 합니다. 다음은 operation.len()의 정의입니다.:

def len(space, w_obj):
    "len(object) -> integer\n\nReturn the number of items of a sequence or mapping."
    return space.len(w_obj)

노출된 인터프리터 수준 함수는 보통 space 인자와 몇 가지 래핑된 값을 받습니다(Wrapping rules 참고).

interpleveldefs 딕셔너리에서는 편리한 단축 표기도 사용할 수 있습니다. 즉, 괄호로 감싼 표현식을 사용하면 (파일에서 간접적으로 가져오는 대신) 인터프리터 수준 표현식을 직접 지정할 수 있습니다.:

...
'None'          : '(space.w_None)',
'False'         : '(space.w_False)',
...

인터프리터 수준 표현식은 실행될 때 space 바인딩을 가집니다.

pypy/module 아래에 새 모듈 항목(예: mymodule)을 추가하면 py.py와 translate.py에 대한 새 설정 옵션(예: –withmod-mymodule 및 –withoutmod-mymodule (후자가 기본값))이 자동으로 생성됩니다.

lib_pypy/에 있는 모듈 테스트

pypy/module/test_lib_pypy/ 디렉터리로 이동한 뒤 테스트 도구(“py.test” 또는 “python ../../pypy/test_all.py”)를 실행하면 lib_pypy 계층을 대상으로 테스트를 실행할 수 있습니다. 이를 통해 Python으로 작성한 재구현을 CPython과 비교하여 빠르게 테스트할 수 있습니다.

pypy/module의 모듈 테스트하기

pypy/module 또는 그 하위 디렉터리로 이동한 다음 평소처럼 테스트를 실행하면 됩니다.

lib-python의 모듈 테스트하기

CPython의 회귀 테스트를 PyPy에서 실행하려면 lib-python/ 디렉터리로 이동한 뒤 테스트 도구를 실행하여 호환성 테스트를 시작할 수 있습니다. (XXX 테스트 보고서 생성과 관련한 Windows 호환성을 확인해야 합니다).

명명 규칙과 디렉터리 구조

디렉터리 및 파일 명명 규칙

  • 디렉터리/모듈/네임스페이스 이름은 항상 소문자로 씁니다
  • 디렉터리 이름과 파일 이름에는 절대 복수형을 사용하지 마십시오.
  • __init__.py는 pypy/objspace/*와 pypy/module/*/__init__.py를 제외하면 보통 비어 있습니다.
  • 디렉터리 중첩은 4단계를 넘지 않게 하십시오.
  • 파일 이름은 간결하고 자동 완성하기 쉽게 유지하십시오.

Python 객체의 이름 지정

  • 클래스 이름은 CamelCase로 씁니다
  • 함수와 메서드 이름은 소문자로 쓰고 _로 단어를 구분합니다
  • 객체 공간(object space) 클래스의 이름은 XyzObjSpace 형식으로 표기합니다. 예를 들어
    • StdObjSpace
    • FlowObjSpace
  • 인터프리터 수준과 ObjSpace에서는 모든 박싱된(boxed) 값이 “래핑된 값(wrapped values)”임을 나타내기 위해 앞에 w_가 붙습니다. 여기에는 w_self도 포함됩니다. 애플리케이션 수준의 순수 Python 코드에서는 w_를 사용하지 마십시오.

저장소에 커밋하기와 브랜치 만들기

  • 여러 사람이 diff를 읽고 있으므로 로그 메시지를 잘 작성하십시오.

  • 이전에 trunk라고 부르던 것을 mercurial에서는 default 브랜치라고 부릅니다. mercurial에서 브랜치는 항상 저장소의 나머지 부분과 함께 푸시됩니다. try1 브랜치를 만들려면(try1이라는 이름의 브랜치가 아직 없다고 가정할 때) 다음과 같이 하십시오:

    hg branch try1
    

    브랜치는 커밋을 한 뒤에야 저장소에 기록됩니다. 기본 브랜치로 다시 전환하려면:

    hg update default
    

    자세한 내용은 도움말을 사용하거나 official wiki를 참조하십시오.:

    hg help branch
    

PyPy에서의 테스트

저희 테스트는 py.test 도구를 기반으로 하며, 이 도구를 사용하면 보일러플레이트 코드 없이 단위 테스트를 작성할 수 있습니다. 한 디렉터리에 있는 모듈의 테스트는 보통 모두 test라는 하위 디렉터리에 있습니다. 단위 테스트에는 기본적으로 두 가지 유형이 있습니다:

  • 인터프리터 수준 테스트. 이 테스트는 PyPy의 인터프리터와 같은 레벨에서 실행됩니다.
  • 애플리케이션 수준 테스트. 이 테스트는 애플리케이션 수준에서 실행됩니다. 즉, 겉보기에는 일반 Python 코드와 같지만 PyPy가 해석합니다.

인터프리터 수준 테스트

테스트 함수와 메서드는 다음과 같이 작성할 수 있습니다:

def test_something(space):
    # use space ...

class TestSomething(object):
    def test_some(self):
        # use 'self.space' here

테스트 함수에는 test 접두사를, 테스트 클래스에는 Test 접두사를 반드시 붙여야 한다는 점에 유의하십시오. 두 경우 모두 py.test 도구를 사용하는 덕분에 모듈 전역 수준에서 Python 모듈을 임포트하고 일반 ‘assert’ 문을 사용할 수 있습니다.

애플리케이션 수준 테스트

PyPy의 적합성과 올바른 동작을 테스트할 때는 특정 코딩 스타일이나 제약을 신경 쓸 필요가 없는 “일반적인” 애플리케이션 수준 Python 코드를 작성하는 것으로 충분한 경우가 많습니다. 선택할 수 있다면 저희는 주로 애플리케이션 수준 테스트를 사용합니다. 이러한 테스트는 이름이 apptest_ 접두사로 시작하는 파일에 들어 있으며 다음과 같은 모습입니다.:

# spaceconfig = {"usemodules":["array"]}
def test_this():
    # application level test code

이러한 애플리케이션 수준 테스트 함수는 PyPy 위에서 실행됩니다. 즉, 인터프리터의 세부 사항에는 접근할 수 없습니다.

기본적으로 이들은 호스트 인터프리터 위에서 실행되는, 변환되지 않은 PyPy 위에서 실행됩니다. -D 옵션을 전달하면 이들은 호스트 인터프리터 위에서 직접 실행되며, 이 경우 호스트 인터프리터는 보통 변환된 pypy 실행 파일입니다.:

pypy3 -m pytest -D pypy/

인터프리트 모드에서는 pytest 기능 중 극히 일부만 사용할 수 있다는 점에 유의하십시오. 호스트 인터프리터는 객체 공간(object space)을 구성하기 위해 선택적인 spaceconfig 선언을 파싱합니다. 이 선언은 유효한 json dict 형식이어야 합니다.

혼합 수준 테스트 (사용 중단됨)

혼합 수준 테스트는 애플리케이션 수준 테스트와 비슷합니다. 다만 인터프리터 수준 테스트 파일 안에 애플리케이션 수준 코드 조각을 넣은 것일 뿐이라는 점이 다릅니다. 예를 들면 다음과 같습니다.:

class AppTestSomething(object):
    def test_this(self):
        # application level test code

전역 수준에서 임포트한 모듈은 사용할 수 없습니다. 이러한 모듈은 인터프리터 수준에서 임포트되지만, 테스트 코드는 애플리케이션 수준에서 실행되기 때문입니다. 모듈을 사용해야 한다면 테스트 함수 안에서 임포트해야 합니다.

AppTest의 setup_class 메서드를 사용하여 AppTest에 데이터를 전달할 수 있습니다. 거기에서 클래스에 연결된 래핑된 객체 가운데 w_로 시작하는 객체는 모두 실제 테스트 메서드에서 self를 통해 (단, w_ 없이) 접근할 수 있습니다. 예를 들면 다음과 같습니다.:

class AppTestErrno(object):
    def setup_class(cls):
        cls.w_d = cls.space.wrap({"a": 1, "b", 2})

    def test_dict(self):
        assert self.d["a"] == 1
        assert self.d["b"] == 2

또 다른 방법은 cls.space.appexec를 사용하는 것입니다. 예를 들면 다음과 같습니다.:

class AppTestSomething(object):
    def setup_class(cls):
        arg = 2
        cls.w_result = cls.space.appexec([cls.space.wrap(arg)], """(arg):
            return arg ** 6
            """)

    def test_power(self):
        assert self.result == 2 ** 6

이 함수는 코드 문자열로 된 함수를 주어진 인자와 함께 앱 레벨에서 실행합니다. setup_class에서는 w_result를 사용하지만 테스트에서는 self.result를 사용한다는 점에 유의하십시오. 다음은 이후의 테스트에서 사용할 수 있는 앱 레벨 클래스를 setup_class에서 정의하는 방법입니다.:

class AppTestSet(object):
    def setup_class(cls):
        w_fakeint = cls.space.appexec([], """():
            class FakeInt(object):
                def __init__(self, value):
                    self.value = value
                def __hash__(self):
                    return hash(self.value)

                def __eq__(self, other):
                    if other == self.value:
                        return True
                    return False
            return FakeInt
            """)
        cls.w_FakeInt = w_fakeint

    def test_fakeint(self):
        f1 = self.FakeInt(4)
        assert f1 == 4
        assert hash(f1) == hash(4)

명령줄 도구 test_all

다음 명령을 실행하면 PyPy의 테스트를 거의 모두 실행할 수 있습니다:

python test_all.py file_or_directory

이는 py/bin/ 디렉터리에 있는 범용 py.test 유틸리티의 동의어입니다. 테스트 실행 방식을 바꾸는 스위치를 보려면 -h 옵션을 전달하십시오.

커버리지 보고서

커버리지 보고서를 얻을 수 있도록 pytest-cov 플러그인이 포함되어 있습니다. 이 플러그인을 사용하려면 추가 요구 사항( coverage 및 cov-core )이 필요하며, 이들을 설치하면 다음과 같이 커버리지 테스트를 실행할 수 있습니다:

python test_all.py --cov file_or_direcory_to_cover file_or_directory

테스트 규칙

  • 기능을 추가하려면 적절한 테스트도 함께 추가해야 합니다. (테스트가 실제로 실패할 수 있는지 확인할 수 있도록 테스트를 먼저 작성하는 편이 합리적인 경우도 많습니다.)
  • pypy 소스 코드 곳곳에는 단위 테스트가 들어 있는 test/ 디렉터리가 있습니다. 이러한 스크립트는 보통 직접 실행할 수 있으며, pypy/test_all.py로 한꺼번에 실행할 수도 있습니다.

문서 및 웹사이트 변경하기

로컬 체크아웃에 있는 문서/웹사이트 파일

PyPy 문서의 대부분은 pypy/doc에 있습니다. ReST 마크업으로 작성된 ‘.rst’ 파일을 편집하거나 추가하기만 하면 됩니다. ReST quickstart 문서도 있지만, 기존 문서를 살펴보며 어떻게 작성되어 있는지 확인하셔도 됩니다.

https://pypy.org/ 웹 사이트는 별도로 관리된다는 점에 유의하십시오. 이 웹 사이트는 https://github.com/pypy/pypy.org 저장소에 있습니다.

문서/웹사이트 변경 사항 자동 테스트

참조 무결성과 ReST 준수 여부는 자동으로 검사합니다. 테스트를 실행하려면 sphinx가 설치되어 있어야 합니다. 그런 다음 로컬에 체크아웃한 문서 디렉터리로 이동하여 Makefile을 실행하십시오.:

cd pypy/doc
make html

실패가 보이지 않는다면 수정한 내용이 적어도 ReST 오류나 잘못된 로컬 참조를 만들지는 않았을 가능성이 높습니다. 이제 문서 디렉터리에 .html 파일이 생성되어 있을 것이므로 브라우저로 열어 볼 수 있습니다!

또한 문서 안의 원격 참조도 확인하려면 다음 명령을 실행하십시오.:

make linkcheck

원격 URL에 접근할 수 있는지 확인합니다.