Search code examples
pythoncachingpytestpython-decoratorsfixtures

pytest fixture with cache and custom decorator


So I am going to need to build out a lot of pytest fixtures with caching. I figure trying to build a decorator that can hold all of the common logic in the fixture (getting and setting the cache primarily) would reduce the amount of code needed to spin up new fixtures.

Here is what I have so far.

def cached_resource(name):
    def decorator_cached_resource(func):
        def request_wrapper(request):
            @functools.wraps(func)
            def wrapper(*args, **kwargs):
                resource = request.config.cache.get(f'resources/{name}', None)
                if not resource:
                    resource = func(*args, **kwargs)
                    request.config.cache.set(f'resources/{name}', resource)
                return resource
            return wrapper
        return request_wrapper
    return decorator_cached_resource


@pytest.fixture(scope='session')
@cached_resource(name='identity-provider')
def cached_identity_provider():
    return app.identity_providers.create(name='test')

@pytest.fixture(scope='session')
@cached_resource(name='token')
def cached_token(cached_identity_provider):
    return app.identity_providers.tokens.create(
        identity_provider_id=cached_identity_provider['id'],
        token_expiration_days=60
    )

I feel like I am close but I am getting this assertion failure. Instead of returning a dict I am getting the function itself returned.

    def test_create(cached_identity_provider):
>       assert isinstance(cached_identity_provider, dict)
E       assert False
E        +  where False = isinstance(<function cached_identity_provider at 0x1057a8310>, dict)

I need the following requirements/objects to make this work.

  • name of the cached resource so I can store it in the cache correctly
  • func representing the function being decorated
  • request from pytest so I can reference that object to store things in the cache
  • args and kwargs which could be used by the various fixtures

I feel that there is some combination of putting this together that should work - I just cannot figure it out yet. The request and func requirements seem pretty set in stone on how these objects are passed in/referenced. I cannot combine them I don't think.

Any help is greatly appreciated!


Solution

  • So I figured it out. I made a change from request to pytestconfig but the key was providing pytestconfig in the fixture method signature. I was getting too over-zealous with trying to remove parameters from the fixture signatures.

    def cached_resource(name):
        def decorator_cached_resource(func):
            @functools.wraps(func)
            def wrapper(pytestconfig, *args, **kwargs):
                resource = pytestconfig.cache.get(f'resources/{name}', None)
                if not resource:
                    resource = func(pytestconfig, *args, **kwargs)
                    pytestconfig.cache.set(f'resources/{name}', resource)
                return resource
            return wrapper
        return decorator_cached_resource
    
    
    @pytest.fixture(scope='session')
    @cached_resource(name='identity-provider')
    def cached_identity_provider(pytestconfig):
        return app.identity_providers.create(name='pythonapiwrappertest')