From b6ec5a575db7e907784bffb1dfb67b61042822af Mon Sep 17 00:00:00 2001 From: holger krekel Date: Sun, 31 Oct 2010 17:41:58 +0100 Subject: [PATCH] get option settings from ini-file. make getting configuration options from conftest.py only an internal feature. --HG-- branch : trunk --- doc/customize.txt | 147 ++++++++++------------------- example/assertion/test_failures.py | 2 - pytest/plugin/config.py | 64 ++++++++----- pytest/plugin/helpconfig.py | 23 +++-- pytest/plugin/pytester.py | 7 ++ pytest/plugin/session.py | 5 +- testing/conftest.py | 3 - testing/plugin/conftest.py | 1 - testing/plugin/test_helpconfig.py | 4 +- testing/test_config.py | 36 ++++++- testing/test_parseopt.py | 11 --- tox.ini | 1 + 12 files changed, 145 insertions(+), 159 deletions(-) diff --git a/doc/customize.txt b/doc/customize.txt index abef35e52..898ee921b 100644 --- a/doc/customize.txt +++ b/doc/customize.txt @@ -5,22 +5,39 @@ Customizing and Extending py.test basic test configuration =================================== -Command line options ---------------------------------- +Command line options and configuration file settings +----------------------------------------------------------------- -You can get help on options and configuration by running:: +You can get help on options and configuration options by running:: - py.test -h # or --help + py.test -h # prints options _and_ config file settings -This will display command line options, ini-settings and conftest.py -settings in your specific environment. +This will display command line and configuration file settings +which were registered by installed plugins. -reading test configuration from ini-files +how test configuration is read from setup/tox ini-files -------------------------------------------------------- -py.test tries to find a configuration INI format file, trying -to find a section ``[pytest]`` in a ``tox.ini`` (or XXX ``pytest.ini`` file). -Possible entries in a ``[pytest]`` section are: +py.test looks for the first ``[pytest]`` section in either the first ``setup.cfg`` or the first ``tox.ini`` file found upwards from the arguments. Example:: + + py.test path/to/testdir + +will look in the following dirs for a config file:: + + path/to/testdir/setup.cfg + path/to/setup.cfg + path/setup.cfg + setup.cfg + ... # up until root of filesystem + path/to/testdir/tox.ini + path/to/tox.ini + path/tox.ini + ... # up until root of filesystem + +If no path was provided at all the current working directory is used for the lookup. + +builtin configuration file options +---------------------------------------------- .. confval:: minversion = VERSTRING @@ -31,28 +48,14 @@ Possible entries in a ``[pytest]`` section are: .. confval:: addargs = OPTS add the specified ``OPTS`` to the set of command line arguments as if they - had been specified by the user. Example:: + had been specified by the user. Example: if you have this ini file content:: - addargs = --maxfail=2 -rf # exit after 2 failures, report fail info + [pytest] + addargs = --maxfail=2 -rf # exit after 2 failures, report fail info + issuing ``py.test test_hello.py`` actually means:: -setting persistent option defaults ------------------------------------- - -py.test will lookup option values in this order: - -* command line -* ``[pytest]`` section in upwards ``setup.cfg`` or ``tox.ini`` files. -* conftest.py files -* environment variables - -To get an overview on existing names and settings type:: - - py.test --help-config - -This will print information about all available options -in your environment, including your local plugins and -command line options. + py.test --maxfail=2 -rf test_hello.py .. _`function arguments`: funcargs.html .. _`extensions`: @@ -70,10 +73,8 @@ extensions and customizations close to test code. local conftest.py plugins -------------------------------------------------------------- -local `conftest.py` plugins are usually automatically loaded and -registered but its contained hooks are only called when collecting or -running tests in files or directories next to or below the ``conftest.py`` -file. Assume the following layout and content of files:: +local ``conftest.py`` plugins contain directory-specific hook implemenations. Its contained runtest- and collection- related hooks are called when collecting or running tests in files or directories next to or below the ``conftest.py`` +file. Example: Assume the following layout and content of files:: a/conftest.py: def pytest_runtest_setup(item): @@ -93,17 +94,18 @@ Here is how you might run it:: py.test a/test_sub.py # will show "setting up" ``py.test`` loads all ``conftest.py`` files upwards from the command -line file arguments. It usually looks up configuration values or hooks -right-to-left, i.e. the closer conftest files are checked before -the further away ones. This means you can have a ``conftest.py`` -in your home directory to provide global configuration values. +line file arguments. It usually performs look up right-to-left, i.e. +the hooks in "closer" conftest files will be called earlier than further +away ones. This means you can even have a ``conftest.py`` file in your home +directory to customize test functionality globally for all of your projects. .. Note:: - if you have ``conftest.py`` files which do not reside in a + If you have ``conftest.py`` files which do not reside in a python package directory (i.e. one containing an ``__init__.py``) then - "import conftest" will be ambigous and should be avoided. If you - ever want to import anything from a ``conftest.py`` file - put it inside a package. You avoid trouble this way. + "import conftest" can be ambigous because there might be other + ``conftest.py`` files as well on your PYTHONPATH or ``sys.path``. + It is good practise for projects to put ``conftest.py`` within a package + scope or to never import anything from the conftest.py file. .. _`named plugins`: plugin/index.html @@ -115,9 +117,6 @@ py.test loads plugin modules at tool startup in the following way: * by loading all plugins registered through `setuptools entry points`_. -* by reading the ``PYTEST_PLUGINS`` environment variable - and importing the comma-separated list of named plugins. - * by pre-scanning the command line for the ``-p name`` option and loading the specified plugin before actual command line parsing. @@ -127,39 +126,17 @@ py.test loads plugin modules at tool startup in the following way: not loaded at tool startup. * by recursively loading all plugins specified by the - ``pytest_plugins`` variable in a ``conftest.py`` file + ``pytest_plugins`` variable in ``conftest.py`` files Requiring/Loading plugins in a test module or plugin ------------------------------------------------------------- -You can specify plugins in a test module or a plugin like this:: +You can require plugins in a test module or a plugin like this:: pytest_plugins = "name1", "name2", When the test module or plugin is loaded the specified plugins -will be loaded. If you specify plugins without the ``pytest_`` -prefix it will be automatically added. All plugin names -must be lowercase. - -.. _`conftest.py plugin`: -.. _`conftestplugin`: - -Writing per-project plugins (conftest.py) ------------------------------------------------------- - -The purpose of :file:`conftest.py` files is to allow project-specific -test customization. They thus make for a good place to implement -project-specific test related features through hooks. For example you may -set the ``collect_ignore`` variable depending on a command line option -by defining the following hook in a ``conftest.py`` file:: - - # ./conftest.py in your root or package dir - collect_ignore = ['hello', 'test_world.py'] - def pytest_addoption(parser): - parser.addoption("--runall", action="store_true", default=False) - def pytest_configure(config): - if config.getvalue("runall"): - collect_ignore[:] = [] +will be loaded. .. _`setuptools entry points`: .. _registered: @@ -357,10 +334,13 @@ Reference of important objects involved in hooks .. autoclass:: pytest.plugin.config.Parser :members: +.. autoclass:: pytest.plugin.session.File + :inherited-members: + .. autoclass:: pytest.plugin.session.Item :inherited-members: -.. autoclass:: pytest.plugin.session.Node +.. autoclass:: pytest.plugin.python.Function :members: .. autoclass:: pytest.plugin.runner.CallInfo @@ -370,30 +350,3 @@ Reference of important objects involved in hooks :members: - -conftest.py configuration files -================================================= - -conftest.py reference docs - -A unique feature of py.test are its ``conftest.py`` files which allow -project and directory specific customizations to testing. - -* `set option defaults`_ - -or set particular variables to influence the testing process: - -* ``pytest_plugins``: list of named plugins to load - -* ``collect_ignore``: list of paths to ignore during test collection, relative to the containing ``conftest.py`` file - -* ``rsyncdirs``: list of to-be-rsynced directories for distributed - testing, relative to the containing ``conftest.py`` file. - -You may put a conftest.py files in your project root directory or into -your package directory if you want to add project-specific test options. - - -.. _`specify funcargs`: funcargs.html#application-setup-tutorial-example - -.. _`set option defaults`: diff --git a/example/assertion/test_failures.py b/example/assertion/test_failures.py index 336d5b586..7fecc345f 100644 --- a/example/assertion/test_failures.py +++ b/example/assertion/test_failures.py @@ -2,8 +2,6 @@ import py failure_demo = py.path.local(__file__).dirpath('failure_demo.py') -pytest_plugins = "pytest_pytester" - def test_failure_demo_fails_properly(testdir): target = testdir.tmpdir.join(failure_demo.basename) failure_demo.copy(target) diff --git a/pytest/plugin/config.py b/pytest/plugin/config.py index 120608045..06dcce639 100644 --- a/pytest/plugin/config.py +++ b/pytest/plugin/config.py @@ -11,7 +11,7 @@ def pytest_cmdline_parse(pluginmanager, args): return config def pytest_addoption(parser): - parser.addini('addargs', 'extra command line arguments') + parser.addini('addargs', 'default command line arguments') parser.addini('minversion', 'minimally required pytest version') class Parser: @@ -72,14 +72,9 @@ class Parser: setattr(option, name, value) return args - def addini(self, name, description): + def addini(self, name, description, type=None): """ add an ini-file option with the given name and description. """ - self._inidict[name] = description - - def setfromini(self, inisection, option): - for name, value in inisection.items(): - assert name in self._inidict - return setattr(option, name, value) + self._inidict[name] = (description, type) class OptionGroup: def __init__(self, name, description="", parser=None): @@ -330,7 +325,6 @@ class Config(object): self._preparse(args) self._parser.hints.extend(self.pluginmanager._hints) args = self._parser.parse_setoption(args, self.option) - self._parser.setfromini(self.inicfg, self.option) if not args: args.append(py.std.os.getcwd()) self.args = args @@ -358,12 +352,26 @@ class Config(object): return py.path.local.make_numbered_dir(prefix=basename, keep=0, rootdir=basetemp, lock_timeout=None) - def getconftest_pathlist(self, name, path=None): - """ return a matching value, which needs to be sequence - of filenames that will be returned as a list of Path - objects (they can be relative to the location - where they were found). - """ + def getini(self, name): + """ return configuration value from an ini file. """ + try: + description, type = self._parser._inidict[name] + except KeyError: + raise ValueError("unknown configuration value: %r" %(name,)) + try: + value = self.inicfg[name] + except KeyError: + return # None indicates nothing found + if type == "pathlist": + dp = py.path.local(self.inicfg.config.path).dirpath() + l = [] + for relpath in py.std.shlex.split(value): + l.append(dp.join(relpath, abs=True)) + return l + else: + return value + + def _getconftest_pathlist(self, name, path=None): try: mod, relroots = self._conftest.rget_with_confmod(name, path) except KeyError: @@ -377,6 +385,21 @@ class Config(object): l.append(relroot) return l + def _getconftest(self, name, path=None, check=False): + if check: + self._checkconftest(name) + return self._conftest.rget(name, path) + + def getvalue(self, name, path=None): + """ return 'name' value looked up from command line 'options'. + (deprecated) if we can't find the option also lookup + the name in a matching conftest file. + """ + try: + return getattr(self.option, name) + except AttributeError: + return self._getconftest(name, path, check=False) + def getvalueorskip(self, name, path=None): """ return getvalue(name) or call py.test.skip if no value exists. """ try: @@ -387,17 +410,6 @@ class Config(object): except KeyError: py.test.skip("no %r value found" %(name,)) - def getvalue(self, name, path=None): - """ return 'name' value looked up from the 'options' - and then from the first conftest file found up - the path (including the path itself). - if path is None, lookup the value in the initial - conftest modules found during command line parsing. - """ - try: - return getattr(self.option, name) - except AttributeError: - return self._conftest.rget(name, path) def getcfg(args, inibasenames): if not args: diff --git a/pytest/plugin/helpconfig.py b/pytest/plugin/helpconfig.py index 203cf494e..96e61ff16 100644 --- a/pytest/plugin/helpconfig.py +++ b/pytest/plugin/helpconfig.py @@ -40,9 +40,8 @@ def showhelp(config): tw.write(config._parser.optparser.format_help()) tw.line() tw.line() - tw.sep( "=", "config file settings") - tw.line("the following values can be defined in [pytest] sections of") - tw.line("setup.cfg or tox.ini files:") + #tw.sep( "=", "config file settings") + tw.line("setup.cfg or tox.ini options to be put into [pytest] section:") tw.line() for name, help in sorted(config._parser._inidict.items()): @@ -50,21 +49,21 @@ def showhelp(config): tw.line(line[:tw.fullwidth]) tw.line() ; tw.line() - #tw.sep( "=", "conftest.py settings") - tw.line("the following values can be defined in conftest.py files") + #tw.sep("=") + return + + tw.line("conftest.py options:") tw.line() - for name, help in conftest_options: + conftestitems = sorted(config._parser._conftestdict.items()) + for name, help in conftest_options + conftestitems: line = " %-15s %s" %(name, help) tw.line(line[:tw.fullwidth]) tw.line() - tw.sep( "=") + #tw.sep( "=") - -conftest_options = ( +conftest_options = [ ('pytest_plugins', 'list of plugin names to load'), - ('collect_ignore', '(relative) paths ignored during collection'), - ('rsyncdirs', 'to-be-rsynced directories for dist-testing'), -) +] def pytest_report_header(config): lines = [] diff --git a/pytest/plugin/pytester.py b/pytest/plugin/pytester.py index 844d2e027..a9d4ea7d2 100644 --- a/pytest/plugin/pytester.py +++ b/pytest/plugin/pytester.py @@ -248,6 +248,13 @@ class TmpTestdir: def makeconftest(self, source): return self.makepyfile(conftest=source) + def makeini(self, source): + return self.makefile('.ini', tox=source) + + def getinicfg(self, source): + p = self.makeini(source) + return py.iniconfig.IniConfig(p)['pytest'] + def makepyfile(self, *args, **kwargs): return self._makefile('.py', args, kwargs) diff --git a/pytest/plugin/session.py b/pytest/plugin/session.py index 95c24ef98..6453b1bdb 100644 --- a/pytest/plugin/session.py +++ b/pytest/plugin/session.py @@ -9,6 +9,7 @@ import pytest import os, sys def pytest_addoption(parser): + group = parser.getgroup("general", "running and selection options") group._addoption('-x', '--exitfirst', action="store_true", default=False, dest="exitfirst", @@ -32,6 +33,7 @@ def pytest_addoption(parser): group.addoption('--basetemp', dest="basetemp", default=None, metavar="dir", help="base temporary directory for this test run.") + def pytest_namespace(): return dict(collect=dict(Item=Item, Collector=Collector, File=File, Directory=Directory)) @@ -64,7 +66,7 @@ def pytest_runtest_mainloop(session): def pytest_ignore_collect(path, config): p = path.dirpath() - ignore_paths = config.getconftest_pathlist("collect_ignore", path=p) + ignore_paths = config._getconftest_pathlist("collect_ignore", path=p) ignore_paths = ignore_paths or [] excludeopt = config.getvalue("ignore") if excludeopt: @@ -445,7 +447,6 @@ class Collector(Node): raise NotImplementedError("abstract") def collect_by_name(self, name): - """ return a child matching the given name, else None. """ for colitem in self._memocollect(): if colitem.name == name: return colitem diff --git a/testing/conftest.py b/testing/conftest.py index 638c37fcc..d10d8edd6 100644 --- a/testing/conftest.py +++ b/testing/conftest.py @@ -2,9 +2,6 @@ import py import sys pytest_plugins = "pytester", -collect_ignore = ['../build', '../doc/_build'] - -rsyncdirs = ['conftest.py', '../pytest', '../doc', '.'] import os, py pid = os.getpid() diff --git a/testing/plugin/conftest.py b/testing/plugin/conftest.py index d2da55eea..57274273b 100644 --- a/testing/plugin/conftest.py +++ b/testing/plugin/conftest.py @@ -1,6 +1,5 @@ import py -pytest_plugins = "pytester" import pytest.plugin plugindir = py.path.local(pytest.plugin.__file__).dirpath() from pytest._core import default_plugins diff --git a/testing/plugin/test_helpconfig.py b/testing/plugin/test_helpconfig.py index 691cb5bf4..5906ccd67 100644 --- a/testing/plugin/test_helpconfig.py +++ b/testing/plugin/test_helpconfig.py @@ -15,8 +15,8 @@ def test_help(testdir): assert result.ret == 0 result.stdout.fnmatch_lines([ "*-v*verbose*", - "*settings*", - "*conftest.py*", + "*setup.cfg*", + "*minversion*", ]) def test_collectattr(): diff --git a/testing/test_config.py b/testing/test_config.py index 871f16955..5c4226ef8 100644 --- a/testing/test_config.py +++ b/testing/test_config.py @@ -37,7 +37,6 @@ class TestParseIni: ]) class TestConfigCmdlineParsing: - def test_parsing_again_fails(self, testdir): config = testdir.reparseconfig([testdir.tmpdir]) py.test.raises(AssertionError, "config.parse([])") @@ -108,13 +107,43 @@ class TestConfigAPI: p = tmpdir.join("conftest.py") p.write("pathlist = ['.', %r]" % str(somepath)) config = testdir.reparseconfig([p]) - assert config.getconftest_pathlist('notexist') is None - pl = config.getconftest_pathlist('pathlist') + assert config._getconftest_pathlist('notexist') is None + pl = config._getconftest_pathlist('pathlist') print(pl) assert len(pl) == 2 assert pl[0] == tmpdir assert pl[1] == somepath + def test_addini(self, testdir): + testdir.makeconftest(""" + def pytest_addoption(parser): + parser.addini("myname", "my new ini value") + """) + testdir.makeini(""" + [pytest] + myname=hello + """) + config = testdir.parseconfig() + val = config.getini("myname") + assert val == "hello" + py.test.raises(ValueError, config.getini, 'other') + + def test_addini_pathlist(self, testdir): + testdir.makeconftest(""" + def pytest_addoption(parser): + parser.addini("paths", "my new ini value", type="pathlist") + parser.addini("abc", "abc value") + """) + p = testdir.makeini(""" + [pytest] + paths=hello world/sub.py + """) + config = testdir.parseconfig() + l = config.getini("paths") + assert len(l) == 2 + assert l[0] == p.dirpath('hello') + assert l[1] == p.dirpath('world/sub.py') + py.test.raises(ValueError, config.getini, 'other') def test_options_on_small_file_do_not_blow_up(testdir): def runfiletest(opts): @@ -151,3 +180,4 @@ def test_preparse_ordering(testdir, monkeypatch): config = testdir.parseconfig() plugin = config.pluginmanager.getplugin("mytestplugin") assert plugin.x == 42 + diff --git a/testing/test_parseopt.py b/testing/test_parseopt.py index fbe800a7f..ab8f6ebca 100644 --- a/testing/test_parseopt.py +++ b/testing/test_parseopt.py @@ -101,17 +101,6 @@ class TestParser: assert option.hello == "world" assert option.this == 42 - def test_parser_addini(self, tmpdir): - parser = parseopt.Parser() - parser.addini("myname", "my new ini value") - cfg = py.iniconfig.IniConfig("tox.ini", dedent(""" - [pytest] - myname=hello - """))['pytest'] - class option: - pass - parser.setfromini(cfg, option) - assert option.myname == "hello" @py.test.mark.skipif("sys.version_info < (2,5)") def test_addoption_parser_epilog(testdir): diff --git a/tox.ini b/tox.ini index 62b4b4905..ca901ee50 100644 --- a/tox.ini +++ b/tox.ini @@ -51,3 +51,4 @@ commands= [pytest] minversion=2.0 +plugins=pytester