pytest-dev / pytest-xdist

pytest plugin for distributed testing and loop-on-failures testing modes.
https://pytest-xdist.readthedocs.io
MIT License
1.4k stars 225 forks source link

`pytest_xdist_auto_num_workers` function cannot be conditionally defined #1102

Open DetachHead opened 2 weeks ago

DetachHead commented 2 weeks ago

the problem

i'm trying to create a pytest_xdist_auto_num_workers hook that should only be registered if the user runs pytest with the xdist plugin enabled, otherwise it will crash due to the hook name being unknown:

according to the pytest docs, i can accomplish it like so:

class DeferPlugin:
    @hookimpl(wrapper=True)
    def pytest_xdist_auto_num_workers(config: pytest.Config) -> Generator[None, int, int]:
        return min((yield), len(config.option.file_or_dir))

def pytest_configure(config):
    if config.pluginmanager.hasplugin("xdist"):
        config.pluginmanager.register(DeferPlugin())

however it doesn't seem to work. the DeferPlugin gets registered but the pytest_xdist_auto_num_workers hook never gets called. i believe this is because pytest_xdist_auto_num_workers gets called before pytest_configure.

attempted workarounds

PYTEST_XDIST_AUTO_NUM_WORKERS environment variable

i considered using the PYTEST_XDIST_AUTO_NUM_WORKERS environment variable, but that won't work in this case because the logic i want to use to determine the new value needs to be based on the default value, which the environment variable does not contain.

attempting to import xdist before registering the hook

i tried doing this:

try:
    import xdist
except ModuleNotFoundError:
    pass
else:

    @pytest.hookimpl(wrapper=True)
    def pytest_xdist_auto_num_workers(config: pytest.Config) -> Generator[None, int, int]:
        return min((yield), len(config.option.file_or_dir))

which works most of the time, but if the user runs pytest with -p no:xdist, it will crash because the xdist module exists but the plugin is disabled, causing it to attempt to register the hook when pytest would not recognize it.

DetachHead commented 2 weeks ago

this seems to work:

class DeferPlugin:
    @pytest.hookimpl(wrapper=True)
    def pytest_xdist_auto_num_workers(self, config: pytest.Config) -> typing.Generator[None, int, int]:
        return min((yield), len(config.option.file_or_dir))

def pytest_plugin_registered(plugin: object, manager: pytest.PytestPluginManager):
    if manager.hasplugin("xdist") and not isinstance(plugin, DeferPlugin):
        manager.register(DeferPlugin())

perhaps the documentation could be updated to mention this, as believe it would be a common use case to only want to register the hook when the plugin is active.