@pytest.fixture와 @pytest.fixture()의 차이
파이썬 데코레이터
- @pytest.fixture
@pytest.fixture
def f():
# do something
# 위의 코드는 다음과 동일함
f = pytest.fixture(f)
-
반면에
- @pytest.fixture()
@pytest.fixture()
def f():
# do something
# 위의 코드는 다음과 동일함
pyfixture = pytest.fixture()
f = pyfixture(f)
# 또는
f = pytest.fixture()(f)
-
둘은 방식의 차이가 분명히 존재한다
-
하지만 실제로 사용해보면 둘의 결과가 같다는 것을 알 수 있다
-
어떻게 가능한 걸까?
-
pytest에서 fixture 함수(=데코레이터 함수)는 기본적으로 2가지 형태로 오버로딩 되어있다
def deco(func):
# do something
-
참고로 데코레이터 함수의 첫 번째 인자는 당연하게도 데코레이터에 넣을 함수이다
@overload
def fixture(
fixture_function: FixtureFunction,
*,
scope: _ScopeName | Callable[[str, Config], _ScopeName] = ...,
params: Iterable[object] | None = ...,
autouse: bool = ...,
ids: Sequence[object | None] | Callable[[Any], object | None] | None = ...,
name: str | None = ...,
) -> FixtureFunction: ...
-
첫 번째 인자인 fixture_function
은 데코레이터에 넣을 함수를 뜻한다 (그냥 func이라 해도 되는데 풀네임으로 적었다 생각하면 됨)
-
그리고 타입은 FixtureFunction이다 (이거 그냥 Callable TypeVar임)
-
그리고 리턴 타입도 FixtureFunction이다
-
일반적인 데코레이터 함수와 동일하다
-
그럼 이제 두 번째 형태를 보자
@overload
def fixture(
fixture_function: None = ...,
*,
scope: _ScopeName | Callable[[str, Config], _ScopeName] = ...,
params: Iterable[object] | None = ...,
autouse: bool = ...,
ids: Sequence[object | None] | Callable[[Any], object | None] | None = ...,
name: str | None = None,
) -> FixtureFunctionMarker: ...
-
첫 번째 꼴과 다름 점은 2가지가 존재한다
1.
fixture_function
의 타입이 None이다
2.
리턴 타입이 FixtureFunctionMarker이다
-
참고로 FixtureFunctionMarker는 클래스
이다
-
자 이제 함수 내부를 살펴보자 (매우 간단함)
def fixture(
fixture_function=None,
*,
scope="function",
params=None,
autouse=False,
ids=None,
name=None,
):
fixture_marker = FixtureFunctionMarker(
scope=scope,
params=tuple(params) if params is not None else None,
autouse=autouse,
ids=None if ids is None else ids if callable(ids) else tuple(ids),
name=name,
_ispytest=True,
)
# Direct decoration
if fixture_function:
return fixture_marker(fixture_function)
return fixture_marker
-
주석과 타입은 가독성을 위해 생략했다
-
일단 fixture_marker를 생성
하고 시작한다
-
그리고 입력으로 fixture_function
이 들어온다면(None이 아니라면) fixture_marker(fixture_function)
을 리턴한다
-
fixture_function
이 None이면 fixture_marker
를 리턴한다
-
[$\star$] 즉, 데코레이터로 @pytest.fixture
를 쓰든 @pytest.fixture()
를 쓰든 결국 fixture_marker(fixture_function)
을 리턴한다 [$\star$]
-
여기서 끝내도 되지만 각 경우 어떤 일이 일어나는지 조금 더 자세히 알아보자
-
@pytest.fixture
인 경우 f = pytest.fixture(f)
라고 했다
-
그러면 if문에서 fixture_function
이 None이 아니고 f
이다
-
그래서 fixture_marker(f)
를 리턴한다
-
여기서 궁금한건 fixture_marker는 뭐냐는 거다
-
일단 fixture_marker는 FixtureFunctionMarker 클래스의 인스턴스다
-
fixture_marker(f)
는 FixtureFunctionMarker 클래스의 __call__ 메서드를 호출한 것이다
-
이제 궁금한건 이 클래스의 __call__ 메서드가 반환하는게 무엇이냐이다
@final
@dataclasses.dataclass(frozen=True)
class FixtureFunctionMarker:
def __call__(self, function: FixtureFunction) -> FixtureFunction:
...
-
위는 FixtureFunctionMarker에서 __call__ 메서드 부분만 따온 것이다
-
__call__ 메서드는 FixtureFunction 타입을 반환하고 이는 아까 말했듯이 그냥 함수이다
-
즉, @pytest.fixture
인 경우 그냥 일반적인 데코레이터와 다를바가 없다
pyfixture = pytest.fixture()
f = pyfixture(f)
-
pyfixture = pytest.fixture()
를 잘보자
-
pytest.fixture()
는 사실 pytest.fixture(fixture_function=None)
과 같다 (함수의 default값을 생각하면 당연함)
-
함수 내부를 보면 if fixture_function
코드가 있음. 근데 여기선 fixture_function이 None이다
-
따라서 해당 if문은 스킵된다
-
그래서 그냥 fixture_marker만 리턴
한다
-
데코레이터가 @pytest.fixture
일 땐 pytest.fixture(f) == fixture_marker(f)
였다
-
즉, pytest.fixture()(f) == fixture_marker(f)
이고 pytest.fixture(f) == fixture_marker(f)
이다
-
pytest.fixture()
로 입력 안하고 pytest.fixture
로 입력하니 자체적으로 pytest.fixture()과 동일한 기능
을 하게 만들어준다
-
따라서 @pytest.fixture()나 @pytest.fixture나 동일한 기능을 수행한다