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?
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:
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.