diff --git a/AUTHORS b/AUTHORS index a19e38bbf13..80fce539294 100644 --- a/AUTHORS +++ b/AUTHORS @@ -92,6 +92,7 @@ Evan Kepner Fabien Zarifian Fabio Zadrozny Feng Ma +Fernando Mezzabotta Rey Florian Bruhin Floris Bruynooghe Gabriel Reis diff --git a/changelog/6870.feature.rst b/changelog/6870.feature.rst new file mode 100644 index 00000000000..e2f365a3317 --- /dev/null +++ b/changelog/6870.feature.rst @@ -0,0 +1 @@ +New ``Config.invocation_args`` attribute containing the unchanged arguments passed to ``pytest.main()``. diff --git a/doc/en/py27-py34-deprecation.rst b/doc/en/py27-py34-deprecation.rst index 95e96de0456..0ca97539c82 100644 --- a/doc/en/py27-py34-deprecation.rst +++ b/doc/en/py27-py34-deprecation.rst @@ -17,9 +17,9 @@ are available on PyPI. While pytest ``5.0`` will be the new mainstream and development version, until **January 2020** the pytest core team plans to make bug-fix releases of the pytest ``4.6`` series by -back-porting patches to the ``4.6-maintenance`` branch that affect Python 2 users. +back-porting patches to the ``4.6.x`` branch that affect Python 2 users. -**After 2020**, the core team will no longer actively backport patches, but the ``4.6-maintenance`` +**After 2020**, the core team will no longer actively backport patches, but the ``4.6.x`` branch will continue to exist so the community itself can contribute patches. The core team will be happy to accept those patches and make new ``4.6`` releases **until mid-2020**. diff --git a/src/_pytest/config/__init__.py b/src/_pytest/config/__init__.py index 63d12104060..0737ff9d51c 100644 --- a/src/_pytest/config/__init__.py +++ b/src/_pytest/config/__init__.py @@ -13,6 +13,7 @@ import types import warnings +import attr import py import six from packaging.version import Version @@ -35,6 +36,7 @@ from _pytest.compat import safe_str from _pytest.outcomes import fail from _pytest.outcomes import Skipped +from _pytest.pathlib import Path from _pytest.warning_types import PytestConfigWarning hookimpl = HookimplMarker("pytest") @@ -154,10 +156,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. @@ -190,7 +197,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: @@ -686,13 +693,52 @@ def _iter_rewritable_modules(package_files): class Config(object): - """ 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 InvocationParams invocation_params: + + Object containing the parameters regarding the ``pytest.main`` + invocation. + 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. + """ + + @attr.s(frozen=True) + class InvocationParams(object): + """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() + dir = attr.ib() + + def __init__(self, pluginmanager, invocation_params=None, *args): + from .argparsing import Parser, FILE_OR_DIR + + if invocation_params is None: + invocation_params = self.InvocationParams( + args=(), plugins=None, dir=Path().resolve() + ) - 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.invocation_params = invocation_params _a = FILE_OR_DIR self._parser = Parser( @@ -709,9 +755,13 @@ 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)) + @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 d0e5937303f..d13f119b020 100644 --- a/testing/test_config.py +++ b/testing/test_config.py @@ -19,6 +19,7 @@ from _pytest.main import EXIT_OK from _pytest.main import EXIT_TESTSFAILED from _pytest.main import EXIT_USAGEERROR +from _pytest.pathlib import Path class TestParseIni(object): @@ -1222,6 +1223,29 @@ def test_config_does_not_load_blocked_plugin_from_args(testdir): assert result.ret == EXIT_USAGEERROR +def test_invocation_args(testdir): + """Ensure that Config.invocation_* arguments are correctly defined""" + + class DummyPlugin(object): + 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_params.args == [p, "-v"] + assert config.invocation_params.dir == Path(str(testdir.tmpdir)) + + 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() + + @pytest.mark.parametrize( "plugin", [