둘이 무슨 차이인데?

  • @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

- 참고로 데코레이터 함수의 첫 번째 인자는 당연하게도 데코레이터에 넣을 함수이다

pytest.fixture 함수

@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_functionNone이면 fixture_marker를 리턴한다

- [$\star$] 즉, 데코레이터로 @pytest.fixture를 쓰든 @pytest.fixture()를 쓰든 결국 fixture_marker(fixture_function)을 리턴한다 [$\star$]

- 여기서 끝내도 되지만 각 경우 어떤 일이 일어나는지 조금 더 자세히 알아보자

@pytest.fixture

- @pytest.fixture인 경우 f = pytest.fixture(f)라고 했다

- 그러면 if문에서 fixture_functionNone이 아니고 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인 경우 그냥 일반적인 데코레이터와 다를바가 없다

@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나 동일한 기능을 수행한다