diff --git a/AUTHORS b/AUTHORS index a53455aff..b4f33914a 100644 --- a/AUTHORS +++ b/AUTHORS @@ -78,6 +78,7 @@ Kale Kundert Katarzyna Jachim Kevin Cox Lee Kamentsky +Lev Maximov Lukas Bednar Maciek Fijalkowski Maho @@ -107,6 +108,7 @@ Punyashloka Biswal Quentin Pradet Ralf Schmitt Raphael Pierzina +Raquel Alegre Roberto Polli Romain Dorgueil Roman Bolshakov @@ -126,6 +128,7 @@ Ted Xiao Thomas Grainger Tom Viner Trevor Bekolay +Tyler Goodlet Vasily Kuznetsov Wouter van Ackooy Xuecong Liao diff --git a/CHANGELOG.rst b/CHANGELOG.rst index e10e19e4f..35fde95bc 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -5,7 +5,6 @@ New Features ------------ - * * @@ -14,8 +13,8 @@ New Features Changes ------- -* Testcase reports with a url attribute will now properly write this to junitxml. - Thanks `@fushi`_ for the PR +* Testcase reports with a ``url`` attribute will now properly write this to junitxml. + Thanks `@fushi`_ for the PR (`#1874`_). * Remove common items from dict comparision output when verbosity=1. Also update the truncation message to make it clearer that pytest truncates all @@ -35,9 +34,44 @@ Changes .. _@mattduck: https://github.com/mattduck .. _#1512: https://github.com/pytest-dev/pytest/issues/1512 +.. _#1874: https://github.com/pytest-dev/pytest/pull/1874 .. _#1952: https://github.com/pytest-dev/pytest/pull/1952 +3.0.3.dev +========= + +* The ``ids`` argument to ``parametrize`` again accepts ``unicode`` strings + in Python 2 (`#1905`_). + Thanks `@philpep`_ for the report and `@nicoddemus`_ for the PR. + +* Assertions are now being rewritten for plugins in development mode + (``pip install -e``) (`#1934`_). + Thanks `@nicoddemus`_ for the PR. + +* Fix pkg_resources import error in Jython projects (`#1853`_). + Thanks `@raquel-ucl`_ for the PR. + +* Got rid of ``AttributeError: 'Module' object has no attribute '_obj'`` exception + in Python 3 (`#1944`_). + Thanks `@axil`_ for the PR. + +* Explain a bad scope value passed to ``@fixture`` declarations or + a ``MetaFunc.parametrize()`` call. Thanks `@tgoodlet`_ for the PR. + + +.. _@philpep: https://github.com/philpep +.. _@raquel-ucl: https://github.com/raquel-ucl +.. _@axil: https://github.com/axil +.. _@tgoodlet: https://github.com/tgoodlet + +.. _#1853: https://github.com/pytest-dev/pytest/issues/1853 +.. _#1905: https://github.com/pytest-dev/pytest/issues/1905 +.. _#1934: https://github.com/pytest-dev/pytest/issues/1934 +.. _#1944: https://github.com/pytest-dev/pytest/issues/1944 + + + 3.0.2 ===== @@ -126,7 +160,11 @@ time or change existing behaviors in order to make them less surprising/more use * Reinterpretation mode has now been removed. Only plain and rewrite mode are available, consequently the ``--assert=reinterp`` option is - no longer available. Thanks `@flub`_ for the PR. + no longer available. This also means files imported from plugins or + ``conftest.py`` will not benefit from improved assertions by + default, you should use ``pytest.register_assert_rewrite()`` to + explicitly turn on assertion rewriting for those files. Thanks + `@flub`_ for the PR. * The following deprecated commandline options were removed: diff --git a/HOWTORELEASE.rst b/HOWTORELEASE.rst index 7da047503..372ecf7f1 100644 --- a/HOWTORELEASE.rst +++ b/HOWTORELEASE.rst @@ -54,8 +54,8 @@ Note: this assumes you have already registered on pypi. 11. Send release announcement to mailing lists: - pytest-dev@python.org - - testing-in-python@lists.idyll.org - python-announce-list@python.org + - testing-in-python@lists.idyll.org (only for minor/major releases) And announce the release on Twitter, making sure to add the hashtag ``#pytest``. diff --git a/_pytest/_code/code.py b/_pytest/_code/code.py index 9a6d32665..30e12940b 100644 --- a/_pytest/_code/code.py +++ b/_pytest/_code/code.py @@ -12,6 +12,7 @@ if sys.version_info[0] >= 3: else: from ._py2traceback import format_exception_only + class Code(object): """ wrapper around Python code objects """ def __init__(self, rawcode): @@ -28,6 +29,8 @@ class Code(object): def __eq__(self, other): return self.raw == other.raw + __hash__ = None + def __ne__(self, other): return not self == other diff --git a/_pytest/_code/source.py b/_pytest/_code/source.py index a1521f8a2..846e3cced 100644 --- a/_pytest/_code/source.py +++ b/_pytest/_code/source.py @@ -52,22 +52,21 @@ class Source(object): return str(self) == other return False + __hash__ = None + def __getitem__(self, key): if isinstance(key, int): return self.lines[key] else: if key.step not in (None, 1): raise IndexError("cannot slice a Source with a step") - return self.__getslice__(key.start, key.stop) + newsource = Source() + newsource.lines = self.lines[key.start:key.stop] + return newsource def __len__(self): return len(self.lines) - def __getslice__(self, start, end): - newsource = Source() - newsource.lines = self.lines[start:end] - return newsource - def strip(self): """ return new source object with trailing and leading blank lines removed. diff --git a/_pytest/assertion/util.py b/_pytest/assertion/util.py index 709fe3b4d..7b4edb6fd 100644 --- a/_pytest/assertion/util.py +++ b/_pytest/assertion/util.py @@ -105,7 +105,7 @@ except NameError: def assertrepr_compare(config, op, left, right): """Return specialised explanations for some operators/operands""" width = 80 - 15 - len(op) - 2 # 15 chars indentation, 1 space around op - left_repr = py.io.saferepr(left, maxsize=int(width/2)) + left_repr = py.io.saferepr(left, maxsize=int(width//2)) right_repr = py.io.saferepr(right, maxsize=width-len(left_repr)) summary = u('%s %s %s') % (ecu(left_repr), op, ecu(right_repr)) diff --git a/_pytest/config.py b/_pytest/config.py index 5b4654a24..661a8513d 100644 --- a/_pytest/config.py +++ b/_pytest/config.py @@ -5,7 +5,6 @@ import traceback import types import warnings -import pkg_resources import py # DON't import pytest here because it causes import cycle troubles import sys, os @@ -952,18 +951,24 @@ class Config(object): except SystemError: mode = 'plain' else: + import pkg_resources self.pluginmanager.rewrite_hook = hook for entrypoint in pkg_resources.iter_entry_points('pytest11'): - for entry in entrypoint.dist._get_metadata('RECORD'): - fn = entry.split(',')[0] - is_simple_module = os.sep not in fn and fn.endswith('.py') - is_package = fn.count(os.sep) == 1 and fn.endswith('__init__.py') - if is_simple_module: - module_name, ext = os.path.splitext(fn) - hook.mark_rewrite(module_name) - elif is_package: - package_name = os.path.dirname(fn) - hook.mark_rewrite(package_name) + # 'RECORD' available for plugins installed normally (pip install) + # 'SOURCES.txt' available for plugins installed in dev mode (pip install -e) + # for installed plugins 'SOURCES.txt' returns an empty list, and vice-versa + # so it shouldn't be an issue + for metadata in ('RECORD', 'SOURCES.txt'): + for entry in entrypoint.dist._get_metadata(metadata): + fn = entry.split(',')[0] + is_simple_module = os.sep not in fn and fn.endswith('.py') + is_package = fn.count(os.sep) == 1 and fn.endswith('__init__.py') + if is_simple_module: + module_name, ext = os.path.splitext(fn) + hook.mark_rewrite(module_name) + elif is_package: + package_name = os.path.dirname(fn) + hook.mark_rewrite(package_name) self._warn_about_missing_assertion(mode) def _warn_about_missing_assertion(self, mode): diff --git a/_pytest/fixtures.py b/_pytest/fixtures.py index cf3e9dd93..3f08b7c6d 100644 --- a/_pytest/fixtures.py +++ b/_pytest/fixtures.py @@ -599,12 +599,29 @@ class ScopeMismatchError(Exception): which has a lower scope (e.g. a Session one calls a function one) """ + scopes = "session module class function".split() scopenum_function = scopes.index("function") + + def scopemismatch(currentscope, newscope): return scopes.index(newscope) > scopes.index(currentscope) +def scope2index(scope, descr, where=None): + """Look up the index of ``scope`` and raise a descriptive value error + if not defined. + """ + try: + return scopes.index(scope) + except ValueError: + raise ValueError( + "{0} {1}has an unsupported scope value '{2}'".format( + descr, 'from {0} '.format(where) if where else '', + scope) + ) + + class FixtureLookupError(LookupError): """ could not return a requested Fixture (missing or invalid). """ def __init__(self, argname, request, msg=None): @@ -703,6 +720,7 @@ def call_fixture_func(fixturefunc, request, kwargs): res = fixturefunc(**kwargs) return res + class FixtureDef: """ A container for a factory definition. """ def __init__(self, fixturemanager, baseid, argname, func, scope, params, @@ -713,7 +731,11 @@ class FixtureDef: self.func = func self.argname = argname self.scope = scope - self.scopenum = scopes.index(scope or "function") + self.scopenum = scope2index( + scope or "function", + descr='fixture {0}'.format(func.__name__), + where=baseid + ) self.params = params startindex = unittest and 1 or None self.argnames = getfuncargnames(func, startindex=startindex) diff --git a/_pytest/mark.py b/_pytest/mark.py index 3c97dc153..d0ead94cd 100644 --- a/_pytest/mark.py +++ b/_pytest/mark.py @@ -25,7 +25,7 @@ def pytest_addoption(parser): help="only run tests which match the given substring expression. " "An expression is a python evaluatable expression " "where all names are substring-matched against test names " - "and their parent classes. Example: -k 'test_method or test " + "and their parent classes. Example: -k 'test_method or test_" "other' matches all test functions and classes whose name " "contains 'test_method' or 'test_other'. " "Additionally keywords are matched to classes and functions " diff --git a/_pytest/python.py b/_pytest/python.py index a4a4dd501..b78d36881 100644 --- a/_pytest/python.py +++ b/_pytest/python.py @@ -205,11 +205,10 @@ class PyobjContext(object): class PyobjMixin(PyobjContext): def obj(): def fget(self): - try: - return self._obj - except AttributeError: + obj = getattr(self, '_obj', None) + if obj is None: self._obj = obj = self._getobj() - return obj + return obj def fset(self, value): self._obj = value return property(fget, fset, None, "underlying python object") @@ -772,7 +771,7 @@ class Metafunc(fixtures.FuncargnamesCompatAttr): It will also override any fixture-function defined scope, allowing to set a dynamic scope using test context or configuration. """ - from _pytest.fixtures import scopes + from _pytest.fixtures import scope2index from _pytest.mark import extract_argvalue from py.io import saferepr @@ -801,7 +800,8 @@ class Metafunc(fixtures.FuncargnamesCompatAttr): if scope is None: scope = _find_parametrized_scope(argnames, self._arg2fixturedefs, indirect) - scopenum = scopes.index(scope) + scopenum = scope2index( + scope, descr='call to {0}'.format(self.parametrize)) valtypes = {} for arg in argnames: if arg not in self.fixturenames: @@ -833,7 +833,7 @@ class Metafunc(fixtures.FuncargnamesCompatAttr): raise ValueError('%d tests specified with %d ids' %( len(argvalues), len(ids))) for id_value in ids: - if id_value is not None and not isinstance(id_value, str): + if id_value is not None and not isinstance(id_value, py.builtin._basestring): msg = 'ids must be list of strings, found: %s (type: %s)' raise ValueError(msg % (saferepr(id_value), type(id_value).__name__)) ids = idmaker(argnames, argvalues, idfn, ids, self.config) @@ -1352,6 +1352,8 @@ class approx(object): return False return all(a == x for a, x in zip(actual, self.expected)) + __hash__ = None + def __ne__(self, actual): return not (actual == self) @@ -1431,6 +1433,8 @@ class ApproxNonIterable(object): # Return true if the two numbers are within the tolerance. return abs(self.expected - actual) <= self.tolerance + __hash__ = None + def __ne__(self, actual): return not (actual == self) diff --git a/appveyor.yml b/appveyor.yml index 38f3d6fad..3144f2095 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -8,7 +8,8 @@ environment: matrix: # create multiple jobs to execute a set of tox runs on each; this is to workaround having # builds timing out in AppVeyor - - TOXENV: "linting,py26,py27,py33,py34,py35,pypy" + # pypy is disabled until #1963 gets fixed + - TOXENV: "linting,py26,py27,py33,py34,py35" - TOXENV: "py27-pexpect,py27-xdist,py27-trial,py35-pexpect,py35-xdist,py35-trial" - TOXENV: "py27-nobyte,doctesting,freeze,docs" diff --git a/doc/en/Makefile b/doc/en/Makefile index 8621f779c..5499c405e 100644 --- a/doc/en/Makefile +++ b/doc/en/Makefile @@ -19,10 +19,9 @@ REGENDOC_ARGS := \ --normalize "@/tmp/pytest-of-.*/pytest-\d+@PYTEST_TMPDIR@" \ - .PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest - + help: @echo "Please use \`make ' where is one of" @echo " html to make standalone HTML files" @@ -36,22 +35,6 @@ help: clean: -rm -rf $(BUILDDIR)/* -SITETARGET=$(shell ./_getdoctarget.py) - -showtarget: - @echo $(SITETARGET) - -install: html - # for access talk to someone with login rights to - # pytest-dev@pytest.org to add your ssh key - rsync -avz _build/html/ pytest-dev@pytest.org:pytest.org/$(SITETARGET) - -installpdf: latexpdf - @scp $(BUILDDIR)/latex/pytest.pdf pytest-dev@pytest.org:pytest.org/$(SITETARGET) - -installall: clean install installpdf - @echo "done" - regen: PYTHONDONTWRITEBYTECODE=1 COLUMNS=76 regendoc --update *.rst */*.rst ${REGENDOC_ARGS} diff --git a/doc/en/_getdoctarget.py b/doc/en/_getdoctarget.py deleted file mode 100755 index 20e487bb7..000000000 --- a/doc/en/_getdoctarget.py +++ /dev/null @@ -1,16 +0,0 @@ -#!/usr/bin/env python - -import py - -def get_version_string(): - fn = py.path.local(__file__).join("..", "..", "..", - "_pytest", "__init__.py") - for line in fn.readlines(): - if "version" in line and not line.strip().startswith('#'): - return eval(line.split("=")[-1]) - -def get_minor_version_string(): - return ".".join(get_version_string().split(".")[:2]) - -if __name__ == "__main__": - print (get_minor_version_string()) diff --git a/doc/en/_templates/links.html b/doc/en/_templates/links.html index 56486a750..d855a013f 100644 --- a/doc/en/_templates/links.html +++ b/doc/en/_templates/links.html @@ -6,6 +6,6 @@
  • pytest @ GitHub
  • 3rd party plugins
  • Issue Tracker
  • -
  • PDF Documentation +
  • PDF Documentation diff --git a/doc/en/conf.py b/doc/en/conf.py index 1c15c17fc..f3b8d7d1e 100644 --- a/doc/en/conf.py +++ b/doc/en/conf.py @@ -19,11 +19,8 @@ # The short X.Y version. import os, sys -sys.path.insert(0, os.path.dirname(__file__)) -import _getdoctarget - -version = _getdoctarget.get_minor_version_string() -release = _getdoctarget.get_version_string() +from _pytest import __version__ as version +release = ".".join(version.split(".")[:2]) # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the diff --git a/doc/en/monkeypatch.rst b/doc/en/monkeypatch.rst index a1287e8dd..e85d94d6d 100644 --- a/doc/en/monkeypatch.rst +++ b/doc/en/monkeypatch.rst @@ -59,6 +59,7 @@ Method reference of the monkeypatch fixture ------------------------------------------- .. autoclass:: MonkeyPatch + :members: ``monkeypatch.setattr/delattr/delitem/delenv()`` all by default raise an Exception if the target does not exist. diff --git a/doc/en/parametrize.rst b/doc/en/parametrize.rst index 9539cb618..825fda8e8 100644 --- a/doc/en/parametrize.rst +++ b/doc/en/parametrize.rst @@ -216,6 +216,3 @@ The **metafunc** object .. currentmodule:: _pytest.python .. autoclass:: Metafunc :members: - - .. automethod:: Metafunc.parametrize - .. automethod:: Metafunc.addcall(funcargs=None,id=_notexists,param=_notexists) diff --git a/doc/en/skipping.rst b/doc/en/skipping.rst index dde89705e..42c43c3dc 100644 --- a/doc/en/skipping.rst +++ b/doc/en/skipping.rst @@ -293,6 +293,20 @@ imperatively, in test or setup code:: # or pytest.skip("unsupported configuration") +Note that calling ``pytest.skip`` at the module level +is not allowed since pytest 3.0. If you are upgrading +and ``pytest.skip`` was being used at the module level, you can set a +``pytestmark`` variable: + +.. code-block:: python + + # before pytest 3.0 + pytest.skip('skipping all tests because of reasons') + # after pytest 3.0 + pytestmark = pytest.mark.skip('skipping all tests because of reasons') + +``pytestmark`` applies a mark or list of marks to all tests in a module. + Skipping on a missing import dependency -------------------------------------------------- @@ -371,3 +385,27 @@ The equivalent with "boolean conditions" is:: imported before pytest's argument parsing takes place. For example, ``conftest.py`` files are imported before command line parsing and thus ``config.getvalue()`` will not execute correctly. + + +Summary +------- + +Here's a quick guide on how to skip tests in a module in different situations: + +1. Skip all tests in a module unconditionally: + + .. code-block:: python + + pytestmark = pytest.mark.skip('all tests still WIP') + +2. Skip all tests in a module based on some condition: + + .. code-block:: python + + pytestmark = pytest.mark.skipif(sys.platform == 'win32', 'tests for linux only') + +3. Skip all tests in a module if some import is missing: + + .. code-block:: python + + pexpect = pytest.importorskip('pexpect') diff --git a/doc/en/unittest.rst b/doc/en/unittest.rst index 3d1ebbbf3..853d93fde 100644 --- a/doc/en/unittest.rst +++ b/doc/en/unittest.rst @@ -38,7 +38,11 @@ the general ``pytest`` documentation for many more examples. Running tests from ``unittest.TestCase`` subclasses with ``--pdb`` will disable tearDown and cleanup methods for the case that an Exception occurs. This allows proper post mortem debugging for all applications - which have significant logic in their tearDown machinery. + which have significant logic in their tearDown machinery. However, + supporting this feature has the following side effect: If people + overwrite ``unittest.TestCase`` ``__call__`` or ``run``, they need to + to overwrite ``debug`` in the same way (this is also true for standard + unittest). Mixing pytest fixtures into unittest.TestCase style tests ----------------------------------------------------------- diff --git a/testing/python/fixture.py b/testing/python/fixture.py index 5ff459cbc..d6b7840c6 100644 --- a/testing/python/fixture.py +++ b/testing/python/fixture.py @@ -1063,6 +1063,22 @@ class TestFixtureUsages: "*1 error*" ]) + def test_invalid_scope(self, testdir): + testdir.makepyfile(""" + import pytest + @pytest.fixture(scope="functions") + def badscope(): + pass + + def test_nothing(badscope): + pass + """) + result = testdir.runpytest_inprocess() + result.stdout.fnmatch_lines( + ("*ValueError: fixture badscope from test_invalid_scope.py has an unsupported" + " scope value 'functions'") + ) + def test_funcarg_parametrized_and_used_twice(self, testdir): testdir.makepyfile(""" import pytest diff --git a/testing/python/metafunc.py b/testing/python/metafunc.py index 58f566973..9d71df20a 100644 --- a/testing/python/metafunc.py +++ b/testing/python/metafunc.py @@ -96,6 +96,14 @@ class TestMetafunc: pytest.raises(ValueError, lambda: metafunc.parametrize("y", [5,6])) pytest.raises(ValueError, lambda: metafunc.parametrize("y", [5,6])) + def test_parametrize_bad_scope(self, testdir): + def func(x): pass + metafunc = self.Metafunc(func) + try: + metafunc.parametrize("x", [1], scope='doggy') + except ValueError as ve: + assert "has an unsupported scope value 'doggy'" in str(ve) + def test_parametrize_and_id(self): def func(x, y): pass metafunc = self.Metafunc(func) @@ -105,6 +113,14 @@ class TestMetafunc: ids = [x.id for x in metafunc._calls] assert ids == ["basic-abc", "basic-def", "advanced-abc", "advanced-def"] + def test_parametrize_and_id_unicode(self): + """Allow unicode strings for "ids" parameter in Python 2 (##1905)""" + def func(x): pass + metafunc = self.Metafunc(func) + metafunc.parametrize("x", [1, 2], ids=[u'basic', u'advanced']) + ids = [x.id for x in metafunc._calls] + assert ids == [u"basic", u"advanced"] + def test_parametrize_with_wrong_number_of_ids(self, testdir): def func(x, y): pass metafunc = self.Metafunc(func) diff --git a/testing/test_assertion.py b/testing/test_assertion.py index 93e8847fa..c6afab014 100644 --- a/testing/test_assertion.py +++ b/testing/test_assertion.py @@ -108,7 +108,8 @@ class TestImportHookInstallation: assert result.ret == 0 @pytest.mark.parametrize('mode', ['plain', 'rewrite']) - def test_installed_plugin_rewrite(self, testdir, mode): + @pytest.mark.parametrize('plugin_state', ['development', 'installed']) + def test_installed_plugin_rewrite(self, testdir, mode, plugin_state): # Make sure the hook is installed early enough so that plugins # installed via setuptools are re-written. testdir.tmpdir.join('hampkg').ensure(dir=1) @@ -135,13 +136,22 @@ class TestImportHookInstallation: 'mainwrapper.py': """ import pytest, pkg_resources + plugin_state = "{plugin_state}" + class DummyDistInfo: project_name = 'spam' version = '1.0' def _get_metadata(self, name): - return ['spamplugin.py,sha256=abc,123', - 'hampkg/__init__.py,sha256=abc,123'] + # 'RECORD' meta-data only available in installed plugins + if name == 'RECORD' and plugin_state == "installed": + return ['spamplugin.py,sha256=abc,123', + 'hampkg/__init__.py,sha256=abc,123'] + # 'SOURCES.txt' meta-data only available for plugins in development mode + elif name == 'SOURCES.txt' and plugin_state == "development": + return ['spamplugin.py', + 'hampkg/__init__.py'] + return [] class DummyEntryPoint: name = 'spam' @@ -159,7 +169,7 @@ class TestImportHookInstallation: pkg_resources.iter_entry_points = iter_entry_points pytest.main() - """, + """.format(plugin_state=plugin_state), 'test_foo.py': """ def test(check_first): check_first([10, 30], 30) @@ -862,3 +872,15 @@ def test_assert_with_unicode(monkeypatch, testdir): """) result = testdir.runpytest() result.stdout.fnmatch_lines(['*AssertionError*']) + +def test_issue_1944(testdir): + testdir.makepyfile(""" + def f(): + return + + assert f() == 10 + """) + result = testdir.runpytest() + result.stdout.fnmatch_lines(["*1 error*"]) + assert "AttributeError: 'Module' object has no attribute '_obj'" not in result.stdout.str() + diff --git a/tox.ini b/tox.ini index 3c12a8da3..ca55e7813 100644 --- a/tox.ini +++ b/tox.ini @@ -10,8 +10,8 @@ envlist= [testenv] commands= pytest --lsof -rfsxX {posargs:testing} passenv = USER USERNAME -deps= - hypothesis +deps= + hypothesis>=3.5.2 nose mock requests @@ -47,7 +47,7 @@ commands = flake8 pytest.py _pytest testing deps=pytest-xdist>=1.13 mock nose - hypothesis + hypothesis>=3.5.2 commands= pytest -n1 -rfsxX {posargs:testing} @@ -71,8 +71,9 @@ commands= pytest -rfsxX test_pdb.py test_terminal.py test_unittest.py [testenv:py27-nobyte] -deps=pytest-xdist>=1.13 - hypothesis +deps= + pytest-xdist>=1.13 + hypothesis>=3.5.2 distribute=true setenv= PYTHONDONTWRITEBYTECODE=1