From 3e669a262a880acdc8a69cad0761e4f1925abe21 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Fri, 5 Jul 2019 19:38:16 -0300 Subject: [PATCH 1/4] Introduce Config.invocation_args and Config.invocation_plugins These attributes can be used to access the unchanged arguments passed to pytest.main(). The intention is to use these attributes to initialize workers in the same manner as the master node is initialized in pytest-xdist. --- changelog/5564.feature.rst | 3 +++ src/_pytest/config/__init__.py | 26 ++++++++++++++++++++------ testing/test_config.py | 22 ++++++++++++++++++++++ 3 files changed, 45 insertions(+), 6 deletions(-) create mode 100644 changelog/5564.feature.rst diff --git a/changelog/5564.feature.rst b/changelog/5564.feature.rst new file mode 100644 index 00000000000..afc9f332323 --- /dev/null +++ b/changelog/5564.feature.rst @@ -0,0 +1,3 @@ +New ``Config.invocation_args`` and ``Config.invocation_plugins`` attributes. + +These attributes can be used by plugins to access the unchanged arguments passed to ``pytest.main()``. diff --git a/src/_pytest/config/__init__.py b/src/_pytest/config/__init__.py index c9310bcb550..688a126d492 100644 --- a/src/_pytest/config/__init__.py +++ b/src/_pytest/config/__init__.py @@ -70,6 +70,8 @@ def main(args=None, plugins=None): tw.line(line.rstrip(), red=True) return 4 else: + config.invocation_args = args + config.invocation_plugins = plugins try: return config.hook.pytest_cmdline_main(config=config) finally: @@ -608,20 +610,33 @@ def _iter_rewritable_modules(package_files): class Config: - """ access to configuration values, pluginmanager and plugin hooks. """ + """ + Access to configuration values, pluginmanager and plugin hooks. + + :ivar PytestPluginManager pluginmanager: the plugin manager handles plugin registration and hook invocation. + + :ivar argparse.Namespace option: access to command line option as attributes. + + :ivar invocation_args: list of command-line arguments as passed to pytest.main() + + :ivar invocation_plugins: list of extra plugins passed to pytest.main(), might be None + + :ivar py.path.local invocation_dir: directory where pytest.main() was invoked from + """ def __init__(self, pluginmanager): - #: access to command line option as attributes. - #: (deprecated), use :py:func:`getoption() <_pytest.config.Config.getoption>` instead - self.option = argparse.Namespace() from .argparsing import Parser, FILE_OR_DIR + self.option = argparse.Namespace() + self.invocation_args = None + self.invocation_plugins = None + self.invocation_dir = py.path.local() + _a = FILE_OR_DIR self._parser = Parser( usage="%(prog)s [options] [{}] [{}] [...]".format(_a, _a), processopt=self._processopt, ) - #: a pluginmanager instance self.pluginmanager = pluginmanager self.trace = self.pluginmanager.trace.root.get("config") self.hook = self.pluginmanager.hook @@ -631,7 +646,6 @@ def __init__(self, pluginmanager): self._cleanup = [] self.pluginmanager.register(self, "pytestconfig") self._configured = False - self.invocation_dir = py.path.local() self.hook.pytest_addoption.call_historic(kwargs=dict(parser=self._parser)) def add_cleanup(self, func): diff --git a/testing/test_config.py b/testing/test_config.py index eb7f9527192..7ec3c9087a2 100644 --- a/testing/test_config.py +++ b/testing/test_config.py @@ -1198,6 +1198,28 @@ def test_config_does_not_load_blocked_plugin_from_args(testdir): assert result.ret == ExitCode.USAGE_ERROR +def test_invocation_arguments(testdir): + """Ensure that Config.invocation_* arguments are correctly defined""" + + class DummyPlugin: + pass + + p = testdir.makepyfile("def test(): pass") + plugin = DummyPlugin() + rec = testdir.inline_run(p, "-v", plugins=[plugin]) + calls = rec.getcalls("pytest_runtest_protocol") + assert len(calls) == 1 + call = calls[0] + config = call.item.config + + assert config.invocation_args == [p, "-v"] + + plugins = config.invocation_plugins + assert len(plugins) == 2 + assert plugins[0] is plugin + assert type(plugins[1]).__name__ == "Collect" # installed by testdir.inline_run() + + @pytest.mark.parametrize( "plugin", [ From 6a9bf2852a8314773c1c04221bb3a0446d35b5d1 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Sat, 6 Jul 2019 12:22:19 -0300 Subject: [PATCH 2/4] Apply review suggestions: use a simple struct for invocation params --- src/_pytest/config/__init__.py | 50 ++++++++++++++++++++++++++-------- testing/test_config.py | 6 ++-- 2 files changed, 42 insertions(+), 14 deletions(-) diff --git a/src/_pytest/config/__init__.py b/src/_pytest/config/__init__.py index 688a126d492..389b0bc81df 100644 --- a/src/_pytest/config/__init__.py +++ b/src/_pytest/config/__init__.py @@ -8,7 +8,9 @@ import types import warnings from functools import lru_cache +from pathlib import Path +import attr import py from packaging.version import Version from pluggy import HookimplMarker @@ -70,8 +72,6 @@ def main(args=None, plugins=None): tw.line(line.rstrip(), red=True) return 4 else: - config.invocation_args = args - config.invocation_plugins = plugins try: return config.hook.pytest_cmdline_main(config=config) finally: @@ -149,10 +149,15 @@ def directory_arg(path, optname): builtin_plugins.add("pytester") -def get_config(args=None): +def get_config(args=None, plugins=None): # subsequent calls to main will create a fresh instance pluginmanager = PytestPluginManager() - config = Config(pluginmanager) + config = Config( + pluginmanager, + invocation_params=Config.InvocationParams( + args=args, plugins=plugins, dir=Path().resolve() + ), + ) if args is not None: # Handle any "-p no:plugin" args. @@ -185,7 +190,7 @@ def _prepareconfig(args=None, plugins=None): msg = "`args` parameter expected to be a list or tuple of strings, got: {!r} (type: {})" raise TypeError(msg.format(args, type(args))) - config = get_config(args) + config = get_config(args, plugins) pluginmanager = config.pluginmanager try: if plugins: @@ -617,20 +622,36 @@ class Config: :ivar argparse.Namespace option: access to command line option as attributes. - :ivar invocation_args: list of command-line arguments as passed to pytest.main() + :ivar InvocationParams invocation_params: - :ivar invocation_plugins: list of extra plugins passed to pytest.main(), might be None + Object containing the parameters regarding the ``pytest.main`` + invocation. - :ivar py.path.local invocation_dir: directory where pytest.main() was invoked from + Contains the followinig read-only attributes: + + * ``args``: list of command-line arguments as passed to ``pytest.main()``. + * ``plugins``: list of extra plugins, might be None + * ``dir``: directory where ``pytest.main()`` was invoked from. """ - def __init__(self, pluginmanager): + @attr.s(frozen=True) + class InvocationParams: + """Holds parameters passed during ``pytest.main()``""" + + args = attr.ib() + plugins = attr.ib() + dir = attr.ib() + + def __init__(self, pluginmanager, *, invocation_params=None): from .argparsing import Parser, FILE_OR_DIR + if invocation_params is None: + invocation_params = self.InvocationParams( + args=(), plugins=None, dir=Path().resolve() + ) + self.option = argparse.Namespace() - self.invocation_args = None - self.invocation_plugins = None - self.invocation_dir = py.path.local() + self.invocation_params = invocation_params _a = FILE_OR_DIR self._parser = Parser( @@ -648,6 +669,11 @@ def __init__(self, pluginmanager): self._configured = False self.hook.pytest_addoption.call_historic(kwargs=dict(parser=self._parser)) + @property + def invocation_dir(self): + """Backward compatibility""" + return py.path.local(str(self.invocation_params.dir)) + def add_cleanup(self, func): """ Add a function to be called when the config object gets out of use (usually coninciding with pytest_unconfigure).""" diff --git a/testing/test_config.py b/testing/test_config.py index 7ec3c9087a2..8ff14cc2009 100644 --- a/testing/test_config.py +++ b/testing/test_config.py @@ -1,5 +1,6 @@ import sys import textwrap +from pathlib import Path import _pytest._code import pytest @@ -1212,9 +1213,10 @@ class DummyPlugin: call = calls[0] config = call.item.config - assert config.invocation_args == [p, "-v"] + assert config.invocation_params.args == [p, "-v"] + assert config.invocation_params.dir == Path(testdir.tmpdir) - plugins = config.invocation_plugins + plugins = config.invocation_params.plugins assert len(plugins) == 2 assert plugins[0] is plugin assert type(plugins[1]).__name__ == "Collect" # installed by testdir.inline_run() From 4cda7093f6362f1d524dfd7d6f4874a4bcdafd7e Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Sat, 6 Jul 2019 17:27:54 -0300 Subject: [PATCH 3/4] Add note about PYTEST_ADDOPTS --- src/_pytest/config/__init__.py | 10 +++++++++- testing/test_config.py | 4 ++-- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/src/_pytest/config/__init__.py b/src/_pytest/config/__init__.py index 389b0bc81df..b5ede4abb51 100644 --- a/src/_pytest/config/__init__.py +++ b/src/_pytest/config/__init__.py @@ -636,7 +636,15 @@ class Config: @attr.s(frozen=True) class InvocationParams: - """Holds parameters passed during ``pytest.main()``""" + """Holds parameters passed during ``pytest.main()`` + + .. note:: + + Currently the environment variable PYTEST_ADDOPTS is also handled by + pytest implicitly, not being part of the invocation. + + Plugins accessing ``InvocationParams`` must be aware of that. + """ args = attr.ib() plugins = attr.ib() diff --git a/testing/test_config.py b/testing/test_config.py index 8ff14cc2009..a00645a4e92 100644 --- a/testing/test_config.py +++ b/testing/test_config.py @@ -1199,7 +1199,7 @@ def test_config_does_not_load_blocked_plugin_from_args(testdir): assert result.ret == ExitCode.USAGE_ERROR -def test_invocation_arguments(testdir): +def test_invocation_args(testdir): """Ensure that Config.invocation_* arguments are correctly defined""" class DummyPlugin: @@ -1214,7 +1214,7 @@ class DummyPlugin: config = call.item.config assert config.invocation_params.args == [p, "-v"] - assert config.invocation_params.dir == Path(testdir.tmpdir) + assert config.invocation_params.dir == Path(str(testdir.tmpdir)) plugins = config.invocation_params.plugins assert len(plugins) == 2 From 7a82285b0330c3ccf3681d51927cced9d9d7778c Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Sat, 6 Jul 2019 17:29:35 -0300 Subject: [PATCH 4/4] Update CHANGELOG --- changelog/5564.feature.rst | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/changelog/5564.feature.rst b/changelog/5564.feature.rst index afc9f332323..e2f365a3317 100644 --- a/changelog/5564.feature.rst +++ b/changelog/5564.feature.rst @@ -1,3 +1 @@ -New ``Config.invocation_args`` and ``Config.invocation_plugins`` attributes. - -These attributes can be used by plugins to access the unchanged arguments passed to ``pytest.main()``. +New ``Config.invocation_args`` attribute containing the unchanged arguments passed to ``pytest.main()``.