diff --git a/CHANGELOG b/CHANGELOG index 76121a175..80b93a01d 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,7 +1,11 @@ Changes between 1.3.4 and 2.0.0dev0 ---------------------------------------------- -- pytest-2.0 is now its own package and depends on pylib +- pytest-2.0 is now its own package and depends on pylib-2.0 +- introduce a new way to set config options via ini-style files, + by default setup.cfg and tox.ini files are searched. The old + ways (certain environment variables, dynamic conftest.py reading + is removed). - fix issue126 - introduce py.test.set_trace() to trace execution via PDB during the running of tests even if capturing is ongoing. - fix issue123 - new "python -m py.test" invocation for py.test diff --git a/doc/conf.py b/doc/conf.py index f1bc56f46..a6c82a057 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -258,3 +258,9 @@ epub_copyright = u'2010, holger krekel et aliter' # Example configuration for intersphinx: refer to the Python standard library. intersphinx_mapping = {'http://docs.python.org/': None} +def setup(app): + #from sphinx.ext.autodoc import cut_lines + #app.connect('autodoc-process-docstring', cut_lines(4, what=['module'])) + app.add_description_unit('confval', 'confval', + objname='configuration value', + indextemplate='pair: %s; configuration value') diff --git a/doc/customize.txt b/doc/customize.txt index 91404806f..47b6433f9 100644 --- a/doc/customize.txt +++ b/doc/customize.txt @@ -5,33 +5,57 @@ Customizing and Extending py.test basic test configuration =================================== -Command line options ---------------------------------- +Command line options and configuration file settings +----------------------------------------------------------------- -You can see command line options by running:: +You can get help on options and configuration options by running:: - py.test -h + py.test -h # prints options _and_ config file settings -This will display all available command line options -in your specific environment. +This will display command line and configuration file settings +which were registered by installed plugins. +how test configuration is read from setup/tox ini-files +-------------------------------------------------------- -setting persistent option defaults ------------------------------------- +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 will lookup option values in this order: + py.test path/to/testdir -* command line -* conftest.py files -* environment variables +will look in the following dirs for a config file:: -To get an overview on existing names and settings type:: + 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 - py.test --help-config +If no path was provided at all the current working directory is used for the lookup. -This will print information about all available options -in your environment, including your local plugins and -command line options. +builtin configuration file options +---------------------------------------------- + +.. confval:: minversion = VERSTRING + + specifies the minimal pytest version that is needed for this test suite. + + minversion = 2.1 # will fail if we run with pytest-2.0 + +.. confval:: addargs = OPTS + + add the specified ``OPTS`` to the set of command line arguments as if they + had been specified by the user. Example: if you have this ini file content:: + + [pytest] + addargs = --maxfail=2 -rf # exit after 2 failures, report fail info + + issuing ``py.test test_hello.py`` actually means:: + + py.test --maxfail=2 -rf test_hello.py .. _`function arguments`: funcargs.html .. _`extensions`: @@ -49,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): @@ -72,20 +94,22 @@ 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 + Plugin discovery at tool startup -------------------------------------------- @@ -93,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. @@ -105,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: @@ -332,12 +331,28 @@ Reference of important objects involved in hooks .. autoclass:: pytest.plugin.config.Config :members: -.. autoclass:: pytest.plugin.session.Item - :inherited-members: - -.. autoclass:: pytest.plugin.session.Node +.. autoclass:: pytest.plugin.config.Parser :members: +.. autoclass:: pytest.plugin.session.Node(name, parent) + :members: + +.. + .. autoclass:: pytest.plugin.session.File(fspath, parent) + :members: + + .. autoclass:: pytest.plugin.session.Item(name, parent) + :members: + + .. autoclass:: pytest.plugin.python.Module(name, parent) + :members: + + .. autoclass:: pytest.plugin.python.Class(name, parent) + :members: + + .. autoclass:: pytest.plugin.python.Function(name, parent) + :members: + .. autoclass:: pytest.plugin.runner.CallInfo :members: @@ -345,29 +360,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/doc/example/marking.txt b/doc/example/controlskip.txt similarity index 94% rename from doc/example/marking.txt rename to doc/example/controlskip.txt index c879931b3..7eb358978 100644 --- a/doc/example/marking.txt +++ b/doc/example/controlskip.txt @@ -1,7 +1,4 @@ -Customizing test function through marks and hooks -==================================================== - .. _`retrieved by hooks as item keywords`: control skipping of tests according to command line option diff --git a/doc/example/misc.txt b/doc/example/detectpytest.txt similarity index 92% rename from doc/example/misc.txt rename to doc/example/detectpytest.txt index 842de7ef4..50490f344 100644 --- a/doc/example/misc.txt +++ b/doc/example/detectpytest.txt @@ -1,7 +1,4 @@ -Misc examples -==================================================== - Detect if running from within a py.test run -------------------------------------------------------------- diff --git a/doc/example/mysetup.txt b/doc/example/mysetup.txt index 60cee65f1..d98f2277e 100644 --- a/doc/example/mysetup.txt +++ b/doc/example/mysetup.txt @@ -1,6 +1,8 @@ .. highlightlang:: python +.. _mysetup: + mysetup pattern: application specific test fixtures ========================================================== diff --git a/doc/example/nonpython.txt b/doc/example/nonpython.txt new file mode 100644 index 000000000..25a40d960 --- /dev/null +++ b/doc/example/nonpython.txt @@ -0,0 +1,68 @@ + +.. _`non-python tests`: + +Working with non-python tests +==================================================== + +a basic example for specifying tests in Yaml files +-------------------------------------------------------------- + +.. _`pytest-yamlwsgi`: http://bitbucket.org/aafshar/pytest-yamlwsgi/src/tip/pytest_yamlwsgi.py +.. _`PyYAML`: http://pypi.python.org/pypi/PyYAML/ + +Here is an example ``conftest.py`` (extracted from Ali Afshnars special purpose `pytest-yamlwsgi`_ plugin). This ``conftest.py`` will collect ``test*.yml`` files and will execute the yaml-formatted content as custom tests: + +.. include:: nonpython/conftest.py + :literal: + +You can create a simple example file: + +.. include:: nonpython/test_simple.yml + :literal: + +and if you installed `PyYAML`_ or a compatible YAML-parser you can +now execute the test specification:: + + nonpython $ py.test + =========================== test session starts ============================ + platform linux2 -- Python 2.6.5 -- pytest-2.0.0.dev10 + test path 1: /home/hpk/p/pytest/doc/example/nonpython + + test_simple.yml .F + + ================================= FAILURES ================================= + ______________________________ usecase: hello ______________________________ + usecase execution failed + spec failed: 'some': 'other' + no further details known at this point. + ==================== 1 failed, 1 passed in 0.06 seconds ==================== + +You get one dot for the passing ``sub1: sub1`` check and one failure. +Obviously in the above ``conftest.py`` you'll want to implement a more +interesting interpretation of the yaml-values. Note that ``reportinfo()`` +is used for representing the test location and is also consulted for +reporting in ``verbose`` mode:: + + nonpython $ py.test -v + =========================== test session starts ============================ + platform linux2 -- Python 2.6.5 -- pytest-2.0.0.dev10 -- /home/hpk/venv/0/bin/python + test path 1: /home/hpk/p/pytest/doc/example/nonpython + + test_simple.yml:1: usecase: ok PASSED + test_simple.yml:1: usecase: hello FAILED + + ================================= FAILURES ================================= + ______________________________ usecase: hello ______________________________ + usecase execution failed + spec failed: 'some': 'other' + no further details known at this point. + ==================== 1 failed, 1 passed in 0.06 seconds ==================== + +While developing your custom test collection and execution it's also +interesting to just look at the collection tree:: + + nonpython $ py.test --collectonly + + + + diff --git a/doc/example/nonpython/conftest.py b/doc/example/nonpython/conftest.py new file mode 100644 index 000000000..8cd15b2f1 --- /dev/null +++ b/doc/example/nonpython/conftest.py @@ -0,0 +1,40 @@ +# content of conftest.py + +import py + +def pytest_collect_file(path, parent): + if path.ext == ".yml" and path.basename.startswith("test"): + return YamlFile(path, parent) + +class YamlFile(py.test.collect.File): + def collect(self): + import yaml # we need a yaml parser, e.g. PyYAML + raw = yaml.load(self.fspath.open()) + for name, spec in raw.items(): + yield UsecaseItem(name, self, spec) + +class UsecaseItem(py.test.collect.Item): + def __init__(self, name, parent, spec): + super(UsecaseItem, self).__init__(name, parent) + self.spec = spec + + def runtest(self): + for name, value in self.spec.items(): + # some custom test execution (dumb example follows) + if name != value: + raise UsecaseException(self, name, value) + + def repr_failure(self, excinfo): + """ called when self.runtest() raises an exception. """ + if excinfo.errisinstance(UsecaseException): + return "\n".join([ + "usecase execution failed", + " spec failed: %r: %r" % excinfo.value.args[1:3], + " no further details known at this point." + ]) + + def reportinfo(self): + return self.fspath, 0, "usecase: %s" % self.name + +class UsecaseException(Exception): + """ custom exception for error reporting. """ diff --git a/doc/example/nonpython/test_simple.yml b/doc/example/nonpython/test_simple.yml new file mode 100644 index 000000000..f0d8d11fc --- /dev/null +++ b/doc/example/nonpython/test_simple.yml @@ -0,0 +1,7 @@ +# test_simple.yml +ok: + sub1: sub1 + +hello: + world: world + some: other diff --git a/doc/examples.txt b/doc/examples.txt index 6a9aa7444..1fc0ced2c 100644 --- a/doc/examples.txt +++ b/doc/examples.txt @@ -7,6 +7,7 @@ Usages and Examples .. toctree:: :maxdepth: 2 - example/marking.txt + example/controlskip.txt example/mysetup.txt - example/misc.txt + example/detectpytest.txt + example/nonpython.txt diff --git a/doc/features.txt b/doc/features.txt index 2a6ede0e5..7828044c7 100644 --- a/doc/features.txt +++ b/doc/features.txt @@ -45,7 +45,8 @@ supports several testing practises and methods - supports extended `xUnit style setup`_ - can integrate nose_, `unittest.py` and `doctest.py`_ style tests -- supports generating testing coverage +- supports generating testing coverage reports +- supports :ref:`non-python tests` - `Javasript unit- and functional testing`_ .. _`Javasript unit- and functional testing`: plugin/oejskit.html diff --git a/doc/goodpractises.txt b/doc/goodpractises.txt index 47b6b43c5..14ea561f2 100644 --- a/doc/goodpractises.txt +++ b/doc/goodpractises.txt @@ -1,4 +1,5 @@ +.. highlightlang:: python .. _`good practises`: Good Practises @@ -28,8 +29,7 @@ py.test supports common test layouts. XXX - - +.. _`genscript method`: Generating a py.test standalone Script ------------------------------------------- @@ -38,12 +38,12 @@ If you are a maintainer or application developer and want users to run tests you can use a facility to generate a standalone "py.test" script that you can tell users to run:: - py.test --genscript=mytest + py.test --genscript=runtests.py will generate a ``mytest`` script that is, in fact, a ``py.test`` under disguise. You can tell people to download and then e.g. run it like this:: - python mytest --pastebin=all + python runtests.py --pastebin=all and ask them to send you the resulting URL. The resulting script has all core features and runs unchanged under Python2 and Python3 interpreters. @@ -51,4 +51,46 @@ all core features and runs unchanged under Python2 and Python3 interpreters. .. _`Distribute for installation`: http://pypi.python.org/pypi/distribute#installation-instructions .. _`distribute installation`: http://pypi.python.org/pypi/distribute + +Integrating with distutils / ``python setup.py test`` +-------------------------------------------------------- + +You can easily integrate test runs into your distutils or +setuptools based project. Use the `genscript method`_ +to generate a standalone py.test script:: + + py.test --genscript=runtests.py + +and make this script part of your distribution and then add +this to your ``setup.py`` file:: + + from distutils.core import setup, Command + # you can also import from setuptools + + class PyTest(Command): + user_options = [] + def initialize_options(self): + pass + def finalize_options(self): + pass + def run(self): + import sys,subprocess + errno = subprocess.call([sys.executable, 'runtest.py']) + raise SystemExit(errno) + setup( + #..., + cmdclass = {'test': PyTest}, + #..., + ) + +If you now type:: + + python setup.py test + +this will execute your tests using ``runtest.py``. As this is a +standalone version of ``py.test`` no prior installation whatsoever is +required for calling the test command. You can also pass additional +arguments to the subprocess-calls like your test directory or other +options. + .. include:: links.inc diff --git a/doc/talks.txt b/doc/talks.txt index 428c16d05..1b8963e5d 100644 --- a/doc/talks.txt +++ b/doc/talks.txt @@ -16,8 +16,8 @@ basic usage and funcargs: function arguments: +- :ref:`mysetup` - `application setup in test functions with funcargs`_ -- `making funcargs dependendent on command line options`_ - `monkey patching done right`_ (blog post, consult `monkeypatch plugin`_ for actual 1.0 API) @@ -39,7 +39,6 @@ plugin specific examples: - `many examples in the docs for plugins`_ .. _`skipping slow tests by default in py.test`: http://bruynooghe.blogspot.com/2009/12/skipping-slow-test-by-default-in-pytest.html -.. _`making funcargs dependendent on command line options`: funcargs.html#tut-cmdlineoption .. _`many examples in the docs for plugins`: plugin/index.html .. _`monkeypatch plugin`: plugin/monkeypatch.html .. _`application setup in test functions with funcargs`: funcargs.html#appsetup 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/__init__.py b/pytest/__init__.py index afc40b3f1..a535a9579 100644 --- a/pytest/__init__.py +++ b/pytest/__init__.py @@ -5,11 +5,12 @@ see http://pytest.org for documentation and details (c) Holger Krekel and others, 2004-2010 """ -__version__ = '2.0.0.dev10' +__version__ = '2.0.0.dev11' __all__ = ['config', 'cmdline'] from pytest import _core as cmdline +UsageError = cmdline.UsageError def __main__(): raise SystemExit(cmdline.main()) \ No newline at end of file diff --git a/pytest/_core.py b/pytest/_core.py index d28ab2643..4022af993 100644 --- a/pytest/_core.py +++ b/pytest/_core.py @@ -345,13 +345,16 @@ def main(args=None): if args is None: args = sys.argv[1:] hook = pluginmanager.hook - config = hook.pytest_cmdline_parse(pluginmanager=pluginmanager, args=args) try: + config = hook.pytest_cmdline_parse( + pluginmanager=pluginmanager, args=args) exitstatus = hook.pytest_cmdline_main(config=config) - except config.Error: + except UsageError: e = sys.exc_info()[1] sys.stderr.write("ERROR: %s\n" %(e.args[0],)) exitstatus = 3 pluginmanager = PluginManager(load=True) return exitstatus +class UsageError(Exception): + """ error in py.test usage or invocation""" diff --git a/pytest/plugin/config.py b/pytest/plugin/config.py index 619e84983..194536a8e 100644 --- a/pytest/plugin/config.py +++ b/pytest/plugin/config.py @@ -2,6 +2,7 @@ import py import sys, os from pytest._core import PluginManager +import pytest def pytest_cmdline_parse(pluginmanager, args): @@ -9,6 +10,10 @@ def pytest_cmdline_parse(pluginmanager, args): config.parse(args) return config +def pytest_addoption(parser): + parser.addini('addargs', 'default command line arguments') + parser.addini('minversion', 'minimally required pytest version') + class Parser: """ Parser for command line arguments. """ @@ -17,6 +22,7 @@ class Parser: self._groups = [] self._processopt = processopt self._usage = usage + self._inidict = {} self.hints = [] def processoption(self, option): @@ -28,6 +34,12 @@ class Parser: self._notes.append(note) def getgroup(self, name, description="", after=None): + """ get (or create) a named option Group. + + :name: unique name of the option group. + :description: long description for --help output. + :after: name of other group, used for ordering --help output. + """ for group in self._groups: if group.name == name: return group @@ -44,7 +56,7 @@ class Parser: self._anonymous.addoption(*opts, **attrs) def parse(self, args): - optparser = MyOptionParser(self) + self.optparser = optparser = MyOptionParser(self) groups = self._groups + [self._anonymous] for group in groups: if group.options: @@ -52,7 +64,7 @@ class Parser: optgroup = py.std.optparse.OptionGroup(optparser, desc) optgroup.add_options(group.options) optparser.add_option_group(optgroup) - return optparser.parse_args([str(x) for x in args]) + return self.optparser.parse_args([str(x) for x in args]) def parse_setoption(self, args, option): parsedoption, args = self.parse(args) @@ -60,6 +72,9 @@ class Parser: setattr(option, name, value) return args + def addini(self, name, description, type=None): + """ add an ini-file option with the given name and description. """ + self._inidict[name] = (description, type) class OptionGroup: def __init__(self, name, description="", parser=None): @@ -90,7 +105,8 @@ class OptionGroup: class MyOptionParser(py.std.optparse.OptionParser): def __init__(self, parser): self._parser = parser - py.std.optparse.OptionParser.__init__(self, usage=parser._usage) + py.std.optparse.OptionParser.__init__(self, usage=parser._usage, + add_help_option=False) def format_epilog(self, formatter): hints = self._parser.hints if hints: @@ -226,13 +242,8 @@ class CmdOptions(object): def __repr__(self): return "" %(self.__dict__,) -class Error(Exception): - """ Test Configuration Error. """ - class Config(object): """ access to configuration values, pluginmanager and plugin hooks. """ - Option = py.std.optparse.Option - Error = Error basetemp = None def __init__(self, pluginmanager=None): @@ -251,6 +262,11 @@ class Config(object): self.trace("loaded conftestmodule %r" %(conftestmodule,)) self.pluginmanager.consider_conftest(conftestmodule) + def _processopt(self, opt): + if hasattr(opt, 'default') and opt.dest: + if not hasattr(self.option, opt.dest): + setattr(self.option, opt.dest, opt.default) + def _getmatchingplugins(self, fspath): allconftests = self._conftest._conftestpath2mod.values() plugins = [x for x in self.pluginmanager.getplugins() @@ -262,28 +278,6 @@ class Config(object): if getattr(self.option, 'traceconfig', None): self.hook.pytest_trace(category="config", msg=msg) - def _processopt(self, opt): - if hasattr(opt, 'default') and opt.dest: - val = os.environ.get("PYTEST_OPTION_" + opt.dest.upper(), None) - if val is not None: - if opt.type == "int": - val = int(val) - elif opt.type == "long": - val = long(val) - elif opt.type == "float": - val = float(val) - elif not opt.type and opt.action in ("store_true", "store_false"): - val = eval(val) - opt.default = val - else: - name = "option_" + opt.dest - try: - opt.default = self._conftest.rget(name) - except (ValueError, KeyError): - pass - if not hasattr(self.option, opt.dest): - setattr(self.option, opt.dest, opt.default) - def _setinitialconftest(self, args): # capture output during conftest init (#issue93) name = hasattr(os, 'dup') and 'StdCaptureFD' or 'StdCapture' @@ -299,14 +293,31 @@ class Config(object): raise def _preparse(self, args): + self.inicfg = getcfg(args, ["setup.cfg", "tox.ini",]) + if self.inicfg: + newargs = self.inicfg.get("addargs", None) + if newargs: + args[:] = args + py.std.shlex.split(newargs) + self._checkversion() self.pluginmanager.consider_setuptools_entrypoints() self.pluginmanager.consider_env() self.pluginmanager.consider_preparse(args) self._setinitialconftest(args) self.pluginmanager.do_addoption(self._parser) + def _checkversion(self): + minver = self.inicfg.get('minversion', None) + if minver: + ver = minver.split(".") + myver = pytest.__version__.split(".") + if myver < ver: + raise pytest.UsageError( + "%s:%d: requires pytest-%s, actual pytest-%s'" %( + self.inicfg.config.path, self.inicfg.lineof('minversion'), + minver, pytest.__version__)) + def parse(self, args): - # cmdline arguments into this config object. + # parse given cmdline arguments into this config object. # Note that this can only be called once per testing process. assert not hasattr(self, 'args'), ( "can only parse cmdline args at most once per Config object") @@ -340,12 +351,28 @@ 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. If the + specified name hasn't been registered through a prior ``parse.addini`` + call (usually from a plugin), a ValueError is raised. """ + 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: @@ -359,6 +386,22 @@ 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 set 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: @@ -369,15 +412,27 @@ 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: + args = [py.path.local()] + for inibasename in inibasenames: + for p in args: + x = findupwards(p, inibasename) + if x is not None: + iniconfig = py.iniconfig.IniConfig(x) + if 'pytest' in iniconfig.sections: + return iniconfig['pytest'] + return {} + +def findupwards(current, basename): + current = py.path.local(current) + while 1: + p = current.join(basename) + if p.check(): + return p + p = current.dirpath() + if p == current: + return + current = p diff --git a/pytest/plugin/genscript.py b/pytest/plugin/genscript.py index 2b379294e..a9188add9 100755 --- a/pytest/plugin/genscript.py +++ b/pytest/plugin/genscript.py @@ -58,7 +58,7 @@ def pytest_cmdline_main(config): genscript = config.getvalue("genscript") if genscript: script = generate_script( - 'import py; py.test.cmdline.main()', + 'import py; raise SystemExit(py.test.cmdline.main())', ['py', 'pytest'], ) diff --git a/pytest/plugin/helpconfig.py b/pytest/plugin/helpconfig.py index 18c810f70..96e61ff16 100644 --- a/pytest/plugin/helpconfig.py +++ b/pytest/plugin/helpconfig.py @@ -1,12 +1,15 @@ """ provide version info, conftest/environment config names. """ import py +import pytest import inspect, sys def pytest_addoption(parser): group = parser.getgroup('debugconfig') group.addoption('--version', action="store_true", - help="display py lib version and import information.") + help="display pytest lib version and import information.") + group._addoption("-h", "--help", action="store_true", dest="help", + help="show help message and configuration info") group._addoption('-p', action="append", dest="plugins", default = [], metavar="name", help="early-load given plugin (multi-allowed).") @@ -19,67 +22,55 @@ def pytest_addoption(parser): group.addoption('--debug', action="store_true", dest="debug", default=False, help="generate and show internal debugging information.") - group.addoption("--help-config", action="store_true", dest="helpconfig", - help="show available conftest.py and ENV-variable names.") def pytest_cmdline_main(config): if config.option.version: - p = py.path.local(py.__file__).dirpath() + p = py.path.local(pytest.__file__).dirpath() sys.stderr.write("This is py.test version %s, imported from %s\n" % (py.__version__, p)) return 0 - elif config.option.helpconfig: + elif config.option.help: config.pluginmanager.do_configure(config) - showpluginhelp(config) + showhelp(config) return 0 -def showpluginhelp(config): - options = [] - for group in config._parser._groups: - options.extend(group.options) - widths = [0] * 10 +def showhelp(config): tw = py.io.TerminalWriter() - tw.sep("-") - tw.line("%-13s | %-18s | %-25s | %s" %( - "cmdline name", "conftest.py name", "ENV-variable name", "help")) - tw.sep("-") + tw.write(config._parser.optparser.format_help()) + tw.line() + tw.line() + #tw.sep( "=", "config file settings") + tw.line("setup.cfg or tox.ini options to be put into [pytest] section:") + tw.line() - options = [opt for opt in options if opt._long_opts] - options.sort(key=lambda x: x._long_opts) - for opt in options: - if not opt._long_opts or not opt.dest: - continue - optstrings = list(opt._long_opts) # + list(opt._short_opts) - optstrings = filter(None, optstrings) - optstring = "|".join(optstrings) - line = "%-13s | %-18s | %-25s | %s" %( - optstring, - "option_%s" % opt.dest, - "PYTEST_OPTION_%s" % opt.dest.upper(), - opt.help and opt.help or "", - ) + for name, help in sorted(config._parser._inidict.items()): + line = " %-15s %s" %(name, help) tw.line(line[:tw.fullwidth]) - for name, help in conftest_options: - line = "%-13s | %-18s | %-25s | %s" %( - "", - name, - "", - help, - ) - tw.line(line[:tw.fullwidth]) - tw.sep("-") -conftest_options = ( + tw.line() ; tw.line() + #tw.sep("=") + return + + tw.line("conftest.py options:") + tw.line() + 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( "=") + +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 = [] if config.option.debug or config.option.traceconfig: - lines.append("using py lib: %s" % (py.path.local(py.__file__).dirpath())) + lines.append("using: pytest-%s pylib-%s" % + (pytest.__version__,py.__version__)) + if config.option.traceconfig: lines.append("active plugins:") plugins = [] @@ -149,12 +140,11 @@ def getargs(func): startindex = inspect.ismethod(func) and 1 or 0 return args[startindex:] -def collectattr(obj, prefixes=("pytest_",)): +def collectattr(obj): methods = {} for apiname in dir(obj): - for prefix in prefixes: - if apiname.startswith(prefix): - methods[apiname] = getattr(obj, apiname) + if apiname.startswith("pytest_"): + methods[apiname] = getattr(obj, apiname) return methods def formatdef(func): diff --git a/pytest/plugin/pytester.py b/pytest/plugin/pytester.py index 7be9be3af..293b300b3 100644 --- a/pytest/plugin/pytester.py +++ b/pytest/plugin/pytester.py @@ -242,9 +242,19 @@ class TmpTestdir: def makefile(self, ext, *args, **kwargs): return self._makefile(ext, args, kwargs) + def makeini(self, source): + return self.makefile('cfg', setup=source) + 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) @@ -375,6 +385,11 @@ class TmpTestdir: #config.pluginmanager.do_unconfigure(config) return node + def collect_by_name(self, modcol, name): + for colitem in modcol._memocollect(): + if colitem.name == name: + return colitem + def popen(self, cmdargs, stdout, stderr, **kw): if not hasattr(py.std, 'subprocess'): py.test.skip("no subprocess module") diff --git a/pytest/plugin/session.py b/pytest/plugin/session.py index c4699f788..422a389f2 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: @@ -128,7 +130,7 @@ class Session(object): config.hook.pytest_sessionstart(session=self) config.hook.pytest_perform_collection(session=self) config.hook.pytest_runtest_mainloop(session=self) - except self.config.Error: + except pytest.UsageError: raise except KeyboardInterrupt: excinfo = py.code.ExceptionInfo() @@ -173,10 +175,10 @@ class Collection: parts = str(arg).split("::") path = base.join(parts[0], abs=True) if not path.check(): - raise self.config.Error("file not found: %s" %(path,)) + raise pytest.UsageError("file not found: %s" %(path,)) topdir = self.topdir if path != topdir and not path.relto(topdir): - raise self.config.Error("path %r is not relative to %r" % + raise pytest.UsageError("path %r is not relative to %r" % (str(path), str(topdir))) topparts = path.relto(topdir).split(path.sep) return topparts + parts[1:] @@ -213,7 +215,7 @@ class Collection: for node in self.matchnodes([self._topcollector], names): items.extend(self.genitems(node)) except NoMatch: - raise self.config.Error("can't collect: %s" % (arg,)) + raise pytest.UsageError("can't collect: %s" % (arg,)) return items def matchnodes(self, matching, names): @@ -444,14 +446,8 @@ 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 - def repr_failure(self, excinfo): - """ represent a failure. """ + """ represent a collection failure. """ if excinfo.errisinstance(self.CollectError): exc = excinfo.value return str(exc.args[0]) @@ -524,8 +520,7 @@ class Directory(FSCollector): class Item(Node): """ a basic test invocation item. Note that for a single function - there might be multiple test invocation items. Attributes: - + there might be multiple test invocation items. """ def reportinfo(self): return self.fspath, None, "" diff --git a/setup.py b/setup.py index 6c79bca6b..8966cdd70 100644 --- a/setup.py +++ b/setup.py @@ -22,7 +22,7 @@ def main(): name='pytest', description='py.test: simple powerful testing with Python', long_description = long_description, - version='2.0.0.dev10', + version='2.0.0.dev11', url='http://pytest.org', license='MIT license', platforms=['unix', 'linux', 'osx', 'cygwin', 'win32'], diff --git a/testing/acceptance_test.py b/testing/acceptance_test.py index ca5d3d991..45d959fb2 100644 --- a/testing/acceptance_test.py +++ b/testing/acceptance_test.py @@ -4,7 +4,8 @@ class TestGeneralUsage: def test_config_error(self, testdir): testdir.makeconftest(""" def pytest_configure(config): - raise config.Error("hello") + import pytest + raise pytest.UsageError("hello") """) result = testdir.runpytest(testdir.tmpdir) assert result.ret != 0 diff --git a/testing/conftest.py b/testing/conftest.py index 45c0d6eb2..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() @@ -41,6 +38,12 @@ def pytest_unconfigure(config, __multicall__): assert len2 < config._numfiles + 7, out2 +def pytest_runtest_setup(item): + item._oldir = py.path.local() + +def pytest_runtest_teardown(item): + item._oldir.chdir() + def pytest_generate_tests(metafunc): multi = getattr(metafunc.function, 'multi', None) if multi is not None: 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_genscript.py b/testing/plugin/test_genscript.py index bb4e555c0..c71ac1526 100644 --- a/testing/plugin/test_genscript.py +++ b/testing/plugin/test_genscript.py @@ -19,13 +19,14 @@ class Standalone: return testdir._run(anypython, self.script, *args) def test_gen(testdir, anypython, standalone): - result = standalone.run(anypython, testdir, '-h') - assert result.ret == 0 result = standalone.run(anypython, testdir, '--version') assert result.ret == 0 result.stderr.fnmatch_lines([ "*imported from*mypytest" ]) + p = testdir.makepyfile("def test_func(): assert 0") + result = standalone.run(anypython, testdir, p) + assert result.ret != 0 @py.test.mark.xfail(reason="fix-dist", run=False) def test_rundist(testdir, pytestconfig, standalone): diff --git a/testing/plugin/test_helpconfig.py b/testing/plugin/test_helpconfig.py index ae49e45f0..5906ccd67 100644 --- a/testing/plugin/test_helpconfig.py +++ b/testing/plugin/test_helpconfig.py @@ -10,11 +10,13 @@ def test_version(testdir): '*py.test*%s*imported from*' % (py.version, ) ]) -def test_helpconfig(testdir): - result = testdir.runpytest("--help-config") +def test_help(testdir): + result = testdir.runpytest("--help") assert result.ret == 0 result.stdout.fnmatch_lines([ - "*cmdline*conftest*ENV*", + "*-v*verbose*", + "*setup.cfg*", + "*minversion*", ]) def test_collectattr(): diff --git a/testing/plugin/test_python.py b/testing/plugin/test_python.py index 069e41d62..b5f344c1d 100644 --- a/testing/plugin/test_python.py +++ b/testing/plugin/test_python.py @@ -268,9 +268,9 @@ class TestSorting: def test_pass(): pass def test_fail(): assert 0 """) - fn1 = modcol.collect_by_name("test_pass") + fn1 = testdir.collect_by_name(modcol, "test_pass") assert isinstance(fn1, py.test.collect.Function) - fn2 = modcol.collect_by_name("test_pass") + fn2 = testdir.collect_by_name(modcol, "test_pass") assert isinstance(fn2, py.test.collect.Function) assert fn1 == fn2 @@ -279,7 +279,7 @@ class TestSorting: assert cmp(fn1, fn2) == 0 assert hash(fn1) == hash(fn2) - fn3 = modcol.collect_by_name("test_fail") + fn3 = testdir.collect_by_name(modcol, "test_fail") assert isinstance(fn3, py.test.collect.Function) assert not (fn1 == fn3) assert fn1 != fn3 @@ -1092,7 +1092,7 @@ class TestReportInfo: class TestClass: def test_hello(self): pass """) - classcol = modcol.collect_by_name("TestClass") + classcol = testdir.collect_by_name(modcol, "TestClass") fspath, lineno, msg = classcol.reportinfo() assert fspath == modcol.fspath assert lineno == 1 @@ -1106,7 +1106,7 @@ class TestReportInfo: assert x yield check, 3 """) - gencol = modcol.collect_by_name("test_gen") + gencol = testdir.collect_by_name(modcol, "test_gen") fspath, lineno, modpath = gencol.reportinfo() assert fspath == modcol.fspath assert lineno == 1 diff --git a/testing/plugin/test_terminal.py b/testing/plugin/test_terminal.py index 5e404f435..244cd0ece 100644 --- a/testing/plugin/test_terminal.py +++ b/testing/plugin/test_terminal.py @@ -466,8 +466,8 @@ def test_getreportopt(): testdict.update(dict(reportchars="sfx")) assert getreportopt(config) == "sfx" -def test_terminalreporter_reportopt_conftestsetting(testdir): - testdir.makeconftest("option_report = 'skipped'") +def test_terminalreporter_reportopt_addargs(testdir): + testdir.makeini("[pytest]\naddargs=-rs") p = testdir.makepyfile(""" def pytest_funcarg__tr(request): tr = request.config.pluginmanager.getplugin("terminalreporter") diff --git a/testing/test_collect.py b/testing/test_collect.py index c045b49c1..f81540870 100644 --- a/testing/test_collect.py +++ b/testing/test_collect.py @@ -28,9 +28,9 @@ class TestCollector: def test_pass(): pass def test_fail(): assert 0 """) - fn1 = modcol.collect_by_name("test_pass") + fn1 = testdir.collect_by_name(modcol, "test_pass") assert isinstance(fn1, py.test.collect.Function) - fn2 = modcol.collect_by_name("test_pass") + fn2 = testdir.collect_by_name(modcol, "test_pass") assert isinstance(fn2, py.test.collect.Function) assert fn1 == fn2 @@ -39,7 +39,7 @@ class TestCollector: assert cmp(fn1, fn2) == 0 assert hash(fn1) == hash(fn2) - fn3 = modcol.collect_by_name("test_fail") + fn3 = testdir.collect_by_name(modcol, "test_fail") assert isinstance(fn3, py.test.collect.Function) assert not (fn1 == fn3) assert fn1 != fn3 @@ -57,8 +57,9 @@ class TestCollector: def test_foo(): pass """) - cls = modcol.collect_by_name("TestClass") - fn = cls.collect_by_name("()").collect_by_name("test_foo") + cls = testdir.collect_by_name(modcol, "TestClass") + fn = testdir.collect_by_name( + testdir.collect_by_name(cls, "()"), "test_foo") parent = fn.getparent(py.test.collect.Module) assert parent is modcol diff --git a/testing/test_config.py b/testing/test_config.py index 23d347dfa..5c4226ef8 100644 --- a/testing/test_config.py +++ b/testing/test_config.py @@ -1,32 +1,42 @@ import py -class TestConfigCmdlineParsing: - def test_parser_addoption_default_env(self, testdir, monkeypatch): - import os - config = testdir.Config() - group = config._parser.getgroup("hello") +from pytest.plugin.config import getcfg, Config - monkeypatch.setitem(os.environ, 'PYTEST_OPTION_OPTION1', 'True') - group.addoption("--option1", action="store_true") - assert group.options[0].default == True +class TestParseIni: + def test_getcfg_and_config(self, tmpdir): + sub = tmpdir.mkdir("sub") + sub.chdir() + tmpdir.join("setup.cfg").write(py.code.Source(""" + [pytest] + name = value + """)) + cfg = getcfg([sub], ["setup.cfg"]) + assert cfg['name'] == "value" + config = Config() + config._preparse([sub]) + assert config.inicfg['name'] == 'value' - monkeypatch.setitem(os.environ, 'PYTEST_OPTION_OPTION2', 'abc') - group.addoption("--option2", action="store", default="x") - assert group.options[1].default == "abc" - - monkeypatch.setitem(os.environ, 'PYTEST_OPTION_OPTION3', '32') - group.addoption("--option3", action="store", type="int") - assert group.options[2].default == 32 - - group.addoption("--option4", action="store", type="int") - assert group.options[3].default == ("NO", "DEFAULT") - - def test_parser_addoption_default_conftest(self, testdir, monkeypatch): - import os - testdir.makeconftest("option_verbose=True") - config = testdir.parseconfig() + def test_append_parse_args(self, tmpdir): + tmpdir.join("setup.cfg").write(py.code.Source(""" + [pytest] + addargs = --verbose + """)) + config = Config() + config.parse([tmpdir]) assert config.option.verbose + def test_tox_ini_wrong_version(self, testdir): + p = testdir.makefile('.ini', tox=""" + [pytest] + minversion=9.0 + """) + result = testdir.runpytest() + assert result.ret != 0 + result.stderr.fnmatch_lines([ + "*tox.ini:2*requires*9.0*actual*" + ]) + +class TestConfigCmdlineParsing: def test_parsing_again_fails(self, testdir): config = testdir.reparseconfig([testdir.tmpdir]) py.test.raises(AssertionError, "config.parse([])") @@ -97,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): @@ -140,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 3d9672e1a..ab8f6ebca 100644 --- a/testing/test_parseopt.py +++ b/testing/test_parseopt.py @@ -1,12 +1,13 @@ import py from pytest.plugin import config as parseopt +from textwrap import dedent class TestParser: - def test_init(self, capsys): + def test_no_help_by_default(self, capsys): parser = parseopt.Parser(usage="xyz") py.test.raises(SystemExit, 'parser.parse(["-h"])') out, err = capsys.readouterr() - assert out.find("xyz") != -1 + assert err.find("no such option") != -1 def test_group_add_and_get(self): parser = parseopt.Parser() @@ -100,6 +101,7 @@ class TestParser: assert option.hello == "world" assert option.this == 42 + @py.test.mark.skipif("sys.version_info < (2,5)") def test_addoption_parser_epilog(testdir): testdir.makeconftest(""" diff --git a/tox.ini b/tox.ini index 982d61158..ca901ee50 100644 --- a/tox.ini +++ b/tox.ini @@ -48,3 +48,7 @@ changedir=testing commands= {envpython} {envbindir}/py.test-jython --no-tools-on-path \ -rfsxX --junitxml={envlogdir}/junit-{envname}2.xml [acceptance_test.py plugin] + +[pytest] +minversion=2.0 +plugins=pytester