Pytest: Accessing the "_pytest.config.Config" object outside of fixtures

Created on 13 Nov 2018  ยท  14Comments  ยท  Source: pytest-dev/pytest

Apologies if this has already been answered, I've searched issues and documentation to the best of my ability without any luck.

I'm happy to do a PR for the docs with the answer.

I'm looking for a solution or best practice when it comes to accessing the Config object outside of fixtures (incl. pytestconfig).

This is how I do it today in a conftest.py file:

import pytest

class Store(object):
    def __init__(self):
        self.config = None

store = Store()

def pytest_namespace():
    return {'store': store}

def pytest_plugin_registered(manager):
    if store.config is None:
        store.config = manager.getplugin('pytestconfig')

And use it like so:

some_option = pytest.store.config.option.some_option

Is there a better way?

question

All 14 comments

most critical dont ever use globals that way ^^

second - pytest-namespace is going to be removed

third - whats the actual use-case

most critical dont ever use globals that way ^^

sorry... ๐Ÿ˜ž ๐Ÿ˜ (to my defense, the code is stolen from another project)

second - pytest-namespace is going to be removed

Oh, bummer. Good I asked then.

third - whats the actual use-case

I'll do my best to try and explain it.

It's only really used in one function. But that function is used in a couple of places.

def get_api_url(api):
    base_url = pytest.store.config.option.base_url
    mapping = {'dpo-auth': {'local': 'http://{}:8001',
                            'dev': 'https://{}:8001',
                            'stage':
                                'https://dpo-auth-app-stage.dporganizer.com',
                            'app':
                                'https://dpo-auth-app-v2.dporganizer.com'},
               'dpo-api':
                   {'local': 'http://{}:8000',
                    'dev': 'https://{}:8000',
                    'stage':
                        'https://dpo-data-app-stage.dporganizer.com'},
               'dpo-manager':
                   {'local': 'http://{}:8003',
                    'dev': 'https://{}:8003',
                    'stage':
                        'https://dpo-manager-app-stage.dporganizer.com'},
               'dpo-api-v2':
                   {'local': 'http://{}:8004',
                    'dev': 'https://{}:8004',
                    'stage':
                        'https://dpo-api-v2-app-stage.dporganizer.com'},
               'widget':
                   {'local': 'http://{}:8005',
                    'dev': 'https://{}:8005',
                    'stage':
                        'https://widget.stage.dporganizer.com'}
               }
    env = base_url.split('.')[0]
    try:
        url = mapping[api][env]
    except KeyError:
        url = mapping[api]['dev']

    return url.format(base_url)

Note: base_url comes from https://github.com/pytest-dev/pytest-base-url via pytest-selenium plugin.

get_api_url is then called from functions that are far removed from any fixtures or totally agnostic to their existence.

An example test:

import pytest
from artifacts.entities import DataAccessPoint
from artifacts.data_flow import DataFlow
from artifacts.subject_category import SubjectCategory
from artifacts.data_storage import DataStorage

pytestmark = pytest.mark.tags('assets')

DATA_STORAGE = 'Test DataStorage'
DATA_ACCESS_POINT = 'Test Data Access Point'


@pytest.mark.feature_flags('assets')
@pytest.mark.user(name='Johannes', account='Supreme account')
@pytest.mark.test_data((SubjectCategory, [{'name': 'Test Subject Category'}]),
                       (DataStorage, [{'name': DATA_STORAGE}]),
                       (DataAccessPoint, [{'name': DATA_ACCESS_POINT}]),
                       (DataFlow, [{'name': 'Test DataFlow',
                                    'data_storage': DATA_STORAGE,
                                    'data_access_point': DATA_ACCESS_POINT}]))
def test_assets(test_data, driver):
    from page_objects.dashboard import DPOCDashboard

    DPOCDashboard(driver) \
        .go_to_url(test_data.user) \
        .select.assets() \
        .verify_assets()

tl;dr: The artifacts have functions that call our API, and need to access the get_api_url. These artifacts are fixture-agnostic, if you will.

Let's start there, maybe you can already give me a better approach.

I'm including the appropriate conftest.py file below (warning: there be dragons).
Details of conftest.py: https://gist.github.com/BeyondEvil/45d7a95d1524e5d0730d21458e562c77

in that situation the starting point is to introduce an object on a location you control that will fail or provide a configured value and configure it in the pytest hooks, afterwards you can work on reshaping the rest of the api's to work fine with a pass trough value or a dependency manager

Thanks @RonnyPfannschmidt !

Not sure I entirely follow. But that's on me. I'll meditate on the problem and your suggested solution for awhile.

Meanwhile, I have two questions:

  1. Is there a specific hook you have in mind or should I introduce a new one?
  2. Do you know of a real world example that you could point me to?

Again, thanks! ๐Ÿ‘

Edit: Also, whatever solution I come up with needs to work with pytest-xdist.

Hey @RonnyPfannschmidt

Here's what I ended up doing:

In the __init__.py file of package utils

class MetaOptions(type):
    options = None  # set in conftest.py::pytest_configure

    def __getattr__(cls, key):
        return getattr(cls.options, key)


class PytestOptions(metaclass=MetaOptions):
    pass

In root conftest.py

def pytest_configure(config):
    from utils import PytestOptions

    PytestOptions.options = config.option

Finally, when used:

def get_api_url(api):
    from utils import PytestOptions

    base_url = PytestOptions.base_url

Seems to work even with xdist.

As for the metaclass notation for PytestOptions class, we only use python 3.6 or newer.

Good? Bad? Ugly? :)

bad and ugly - you are at best lucky if this doesn't break and it will certainly break when using pytester to write acceptance tests for pytest plugins

oh, additionally the meta-class really isn't needed, you can just make a instance of a normal class

bad and ugly

Haha, harsh, man! ๐Ÿ˜‚

you are at best lucky if this doesn't break

How would it break? For complex objects?
It works now, even when using xdist.

it will certainly break when using pytester to write acceptance tests for pytest plugins

That's not a problem, the project will never be a plugin or use pytester.

oh, additionally the meta-class really isn't needed, you can just make a instance of a normal class

Yeah, I wanted to avoid having to do PytestOptions().base_url. But I can't really justify the added complexity of the metaclass over not having to do () ๐Ÿคทโ€โ™‚๏ธ

@BeyondEvil

class FOO(metaclass=BAR): ...

can just be done as

FOO = BAR() - you can have a global variable just fine if that is what you use the "class" for

@RonnyPfannschmidt

Ah, so in that case having a global is fine?

@BeyondEvil its not a suggested solution, but using a instance is better than making a "class" one

@RonnyPfannschmidt

So can you suggest a solution? ๐Ÿ˜Š

the suggested solution would be having the api layered in a way you can correctly pass trough information and control lifetimes

this could be have many forms of expression (including using something like pythons newly introduced context vars

Context vars would be nice, but I have to support 3.6 for a while. :(

I don't see a worthy solution in front of me. Regardless, it would require a major refactor. Which I'm happy to do - but it'll have to wait.

I'll report back whenever I come up with something.

Thank you for your time, I appreciate it.

Was this page helpful?
0 / 5 - 0 ratings