Merge remote-tracking branch 'upstream/master' into features

This commit is contained in:
Bruno Oliveira 2016-09-26 19:32:53 -03:00
commit 654af0ba25
23 changed files with 220 additions and 86 deletions

View File

@ -78,6 +78,7 @@ Kale Kundert
Katarzyna Jachim Katarzyna Jachim
Kevin Cox Kevin Cox
Lee Kamentsky Lee Kamentsky
Lev Maximov
Lukas Bednar Lukas Bednar
Maciek Fijalkowski Maciek Fijalkowski
Maho Maho
@ -107,6 +108,7 @@ Punyashloka Biswal
Quentin Pradet Quentin Pradet
Ralf Schmitt Ralf Schmitt
Raphael Pierzina Raphael Pierzina
Raquel Alegre
Roberto Polli Roberto Polli
Romain Dorgueil Romain Dorgueil
Roman Bolshakov Roman Bolshakov
@ -126,6 +128,7 @@ Ted Xiao
Thomas Grainger Thomas Grainger
Tom Viner Tom Viner
Trevor Bekolay Trevor Bekolay
Tyler Goodlet
Vasily Kuznetsov Vasily Kuznetsov
Wouter van Ackooy Wouter van Ackooy
Xuecong Liao Xuecong Liao

View File

@ -5,7 +5,6 @@
New Features New Features
------------ ------------
* *
* *
@ -14,8 +13,8 @@ New Features
Changes Changes
------- -------
* Testcase reports with a url attribute will now properly write this to junitxml. * Testcase reports with a ``url`` attribute will now properly write this to junitxml.
Thanks `@fushi`_ for the PR Thanks `@fushi`_ for the PR (`#1874`_).
* Remove common items from dict comparision output when verbosity=1. Also update * Remove common items from dict comparision output when verbosity=1. Also update
the truncation message to make it clearer that pytest truncates all the truncation message to make it clearer that pytest truncates all
@ -35,9 +34,44 @@ Changes
.. _@mattduck: https://github.com/mattduck .. _@mattduck: https://github.com/mattduck
.. _#1512: https://github.com/pytest-dev/pytest/issues/1512 .. _#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 .. _#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 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 * Reinterpretation mode has now been removed. Only plain and rewrite
mode are available, consequently the ``--assert=reinterp`` option is 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: * The following deprecated commandline options were removed:

View File

@ -54,8 +54,8 @@ Note: this assumes you have already registered on pypi.
11. Send release announcement to mailing lists: 11. Send release announcement to mailing lists:
- pytest-dev@python.org - pytest-dev@python.org
- testing-in-python@lists.idyll.org
- python-announce-list@python.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``. And announce the release on Twitter, making sure to add the hashtag ``#pytest``.

View File

@ -12,6 +12,7 @@ if sys.version_info[0] >= 3:
else: else:
from ._py2traceback import format_exception_only from ._py2traceback import format_exception_only
class Code(object): class Code(object):
""" wrapper around Python code objects """ """ wrapper around Python code objects """
def __init__(self, rawcode): def __init__(self, rawcode):
@ -28,6 +29,8 @@ class Code(object):
def __eq__(self, other): def __eq__(self, other):
return self.raw == other.raw return self.raw == other.raw
__hash__ = None
def __ne__(self, other): def __ne__(self, other):
return not self == other return not self == other

View File

@ -52,22 +52,21 @@ class Source(object):
return str(self) == other return str(self) == other
return False return False
__hash__ = None
def __getitem__(self, key): def __getitem__(self, key):
if isinstance(key, int): if isinstance(key, int):
return self.lines[key] return self.lines[key]
else: else:
if key.step not in (None, 1): if key.step not in (None, 1):
raise IndexError("cannot slice a Source with a step") 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): def __len__(self):
return len(self.lines) return len(self.lines)
def __getslice__(self, start, end):
newsource = Source()
newsource.lines = self.lines[start:end]
return newsource
def strip(self): def strip(self):
""" return new source object with trailing """ return new source object with trailing
and leading blank lines removed. and leading blank lines removed.

View File

@ -105,7 +105,7 @@ except NameError:
def assertrepr_compare(config, op, left, right): def assertrepr_compare(config, op, left, right):
"""Return specialised explanations for some operators/operands""" """Return specialised explanations for some operators/operands"""
width = 80 - 15 - len(op) - 2 # 15 chars indentation, 1 space around op 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)) right_repr = py.io.saferepr(right, maxsize=width-len(left_repr))
summary = u('%s %s %s') % (ecu(left_repr), op, ecu(right_repr)) summary = u('%s %s %s') % (ecu(left_repr), op, ecu(right_repr))

View File

@ -5,7 +5,6 @@ import traceback
import types import types
import warnings import warnings
import pkg_resources
import py import py
# DON't import pytest here because it causes import cycle troubles # DON't import pytest here because it causes import cycle troubles
import sys, os import sys, os
@ -952,18 +951,24 @@ class Config(object):
except SystemError: except SystemError:
mode = 'plain' mode = 'plain'
else: else:
import pkg_resources
self.pluginmanager.rewrite_hook = hook self.pluginmanager.rewrite_hook = hook
for entrypoint in pkg_resources.iter_entry_points('pytest11'): for entrypoint in pkg_resources.iter_entry_points('pytest11'):
for entry in entrypoint.dist._get_metadata('RECORD'): # 'RECORD' available for plugins installed normally (pip install)
fn = entry.split(',')[0] # 'SOURCES.txt' available for plugins installed in dev mode (pip install -e)
is_simple_module = os.sep not in fn and fn.endswith('.py') # for installed plugins 'SOURCES.txt' returns an empty list, and vice-versa
is_package = fn.count(os.sep) == 1 and fn.endswith('__init__.py') # so it shouldn't be an issue
if is_simple_module: for metadata in ('RECORD', 'SOURCES.txt'):
module_name, ext = os.path.splitext(fn) for entry in entrypoint.dist._get_metadata(metadata):
hook.mark_rewrite(module_name) fn = entry.split(',')[0]
elif is_package: is_simple_module = os.sep not in fn and fn.endswith('.py')
package_name = os.path.dirname(fn) is_package = fn.count(os.sep) == 1 and fn.endswith('__init__.py')
hook.mark_rewrite(package_name) 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) self._warn_about_missing_assertion(mode)
def _warn_about_missing_assertion(self, mode): def _warn_about_missing_assertion(self, mode):

View File

@ -599,12 +599,29 @@ class ScopeMismatchError(Exception):
which has a lower scope (e.g. a Session one calls a function one) which has a lower scope (e.g. a Session one calls a function one)
""" """
scopes = "session module class function".split() scopes = "session module class function".split()
scopenum_function = scopes.index("function") scopenum_function = scopes.index("function")
def scopemismatch(currentscope, newscope): def scopemismatch(currentscope, newscope):
return scopes.index(newscope) > scopes.index(currentscope) 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): class FixtureLookupError(LookupError):
""" could not return a requested Fixture (missing or invalid). """ """ could not return a requested Fixture (missing or invalid). """
def __init__(self, argname, request, msg=None): def __init__(self, argname, request, msg=None):
@ -703,6 +720,7 @@ def call_fixture_func(fixturefunc, request, kwargs):
res = fixturefunc(**kwargs) res = fixturefunc(**kwargs)
return res return res
class FixtureDef: class FixtureDef:
""" A container for a factory definition. """ """ A container for a factory definition. """
def __init__(self, fixturemanager, baseid, argname, func, scope, params, def __init__(self, fixturemanager, baseid, argname, func, scope, params,
@ -713,7 +731,11 @@ class FixtureDef:
self.func = func self.func = func
self.argname = argname self.argname = argname
self.scope = scope 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 self.params = params
startindex = unittest and 1 or None startindex = unittest and 1 or None
self.argnames = getfuncargnames(func, startindex=startindex) self.argnames = getfuncargnames(func, startindex=startindex)

View File

@ -25,7 +25,7 @@ def pytest_addoption(parser):
help="only run tests which match the given substring expression. " help="only run tests which match the given substring expression. "
"An expression is a python evaluatable expression " "An expression is a python evaluatable expression "
"where all names are substring-matched against test names " "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 " "other' matches all test functions and classes whose name "
"contains 'test_method' or 'test_other'. " "contains 'test_method' or 'test_other'. "
"Additionally keywords are matched to classes and functions " "Additionally keywords are matched to classes and functions "

View File

@ -205,11 +205,10 @@ class PyobjContext(object):
class PyobjMixin(PyobjContext): class PyobjMixin(PyobjContext):
def obj(): def obj():
def fget(self): def fget(self):
try: obj = getattr(self, '_obj', None)
return self._obj if obj is None:
except AttributeError:
self._obj = obj = self._getobj() self._obj = obj = self._getobj()
return obj return obj
def fset(self, value): def fset(self, value):
self._obj = value self._obj = value
return property(fget, fset, None, "underlying python object") 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 It will also override any fixture-function defined scope, allowing
to set a dynamic scope using test context or configuration. 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 _pytest.mark import extract_argvalue
from py.io import saferepr from py.io import saferepr
@ -801,7 +800,8 @@ class Metafunc(fixtures.FuncargnamesCompatAttr):
if scope is None: if scope is None:
scope = _find_parametrized_scope(argnames, self._arg2fixturedefs, indirect) scope = _find_parametrized_scope(argnames, self._arg2fixturedefs, indirect)
scopenum = scopes.index(scope) scopenum = scope2index(
scope, descr='call to {0}'.format(self.parametrize))
valtypes = {} valtypes = {}
for arg in argnames: for arg in argnames:
if arg not in self.fixturenames: if arg not in self.fixturenames:
@ -833,7 +833,7 @@ class Metafunc(fixtures.FuncargnamesCompatAttr):
raise ValueError('%d tests specified with %d ids' %( raise ValueError('%d tests specified with %d ids' %(
len(argvalues), len(ids))) len(argvalues), len(ids)))
for id_value in 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)' msg = 'ids must be list of strings, found: %s (type: %s)'
raise ValueError(msg % (saferepr(id_value), type(id_value).__name__)) raise ValueError(msg % (saferepr(id_value), type(id_value).__name__))
ids = idmaker(argnames, argvalues, idfn, ids, self.config) ids = idmaker(argnames, argvalues, idfn, ids, self.config)
@ -1352,6 +1352,8 @@ class approx(object):
return False return False
return all(a == x for a, x in zip(actual, self.expected)) return all(a == x for a, x in zip(actual, self.expected))
__hash__ = None
def __ne__(self, actual): def __ne__(self, actual):
return not (actual == self) return not (actual == self)
@ -1431,6 +1433,8 @@ class ApproxNonIterable(object):
# Return true if the two numbers are within the tolerance. # Return true if the two numbers are within the tolerance.
return abs(self.expected - actual) <= self.tolerance return abs(self.expected - actual) <= self.tolerance
__hash__ = None
def __ne__(self, actual): def __ne__(self, actual):
return not (actual == self) return not (actual == self)

View File

@ -8,7 +8,8 @@ environment:
matrix: matrix:
# create multiple jobs to execute a set of tox runs on each; this is to workaround having # create multiple jobs to execute a set of tox runs on each; this is to workaround having
# builds timing out in AppVeyor # 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-pexpect,py27-xdist,py27-trial,py35-pexpect,py35-xdist,py35-trial"
- TOXENV: "py27-nobyte,doctesting,freeze,docs" - TOXENV: "py27-nobyte,doctesting,freeze,docs"

View File

@ -19,10 +19,9 @@ REGENDOC_ARGS := \
--normalize "@/tmp/pytest-of-.*/pytest-\d+@PYTEST_TMPDIR@" \ --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 .PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest
help: help:
@echo "Please use \`make <target>' where <target> is one of" @echo "Please use \`make <target>' where <target> is one of"
@echo " html to make standalone HTML files" @echo " html to make standalone HTML files"
@ -36,22 +35,6 @@ help:
clean: clean:
-rm -rf $(BUILDDIR)/* -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: regen:
PYTHONDONTWRITEBYTECODE=1 COLUMNS=76 regendoc --update *.rst */*.rst ${REGENDOC_ARGS} PYTHONDONTWRITEBYTECODE=1 COLUMNS=76 regendoc --update *.rst */*.rst ${REGENDOC_ARGS}

View File

@ -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())

View File

@ -6,6 +6,6 @@
<li><a href="https://github.com/pytest-dev/pytest/">pytest @ GitHub</a></li> <li><a href="https://github.com/pytest-dev/pytest/">pytest @ GitHub</a></li>
<li><a href="http://plugincompat.herokuapp.com/">3rd party plugins</a></li> <li><a href="http://plugincompat.herokuapp.com/">3rd party plugins</a></li>
<li><a href="https://github.com/pytest-dev/pytest/issues">Issue Tracker</a></li> <li><a href="https://github.com/pytest-dev/pytest/issues">Issue Tracker</a></li>
<li><a href="http://pytest.org/latest/pytest.pdf">PDF Documentation</a> <li><a href="https://media.readthedocs.org/pdf/pytest/latest/pytest.pdf">PDF Documentation</a>
</ul> </ul>

View File

@ -19,11 +19,8 @@
# The short X.Y version. # The short X.Y version.
import os, sys import os, sys
sys.path.insert(0, os.path.dirname(__file__)) from _pytest import __version__ as version
import _getdoctarget release = ".".join(version.split(".")[:2])
version = _getdoctarget.get_minor_version_string()
release = _getdoctarget.get_version_string()
# If extensions (or modules to document with autodoc) are in another directory, # 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 # add these directories to sys.path here. If the directory is relative to the

View File

@ -59,6 +59,7 @@ Method reference of the monkeypatch fixture
------------------------------------------- -------------------------------------------
.. autoclass:: MonkeyPatch .. autoclass:: MonkeyPatch
:members:
``monkeypatch.setattr/delattr/delitem/delenv()`` all ``monkeypatch.setattr/delattr/delitem/delenv()`` all
by default raise an Exception if the target does not exist. by default raise an Exception if the target does not exist.

View File

@ -216,6 +216,3 @@ The **metafunc** object
.. currentmodule:: _pytest.python .. currentmodule:: _pytest.python
.. autoclass:: Metafunc .. autoclass:: Metafunc
:members: :members:
.. automethod:: Metafunc.parametrize
.. automethod:: Metafunc.addcall(funcargs=None,id=_notexists,param=_notexists)

View File

@ -293,6 +293,20 @@ imperatively, in test or setup code::
# or # or
pytest.skip("unsupported configuration") 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 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, imported before pytest's argument parsing takes place. For example,
``conftest.py`` files are imported before command line parsing and thus ``conftest.py`` files are imported before command line parsing and thus
``config.getvalue()`` will not execute correctly. ``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')

View File

@ -38,7 +38,11 @@ the general ``pytest`` documentation for many more examples.
Running tests from ``unittest.TestCase`` subclasses with ``--pdb`` will Running tests from ``unittest.TestCase`` subclasses with ``--pdb`` will
disable tearDown and cleanup methods for the case that an Exception disable tearDown and cleanup methods for the case that an Exception
occurs. This allows proper post mortem debugging for all applications 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 Mixing pytest fixtures into unittest.TestCase style tests
----------------------------------------------------------- -----------------------------------------------------------

View File

@ -1063,6 +1063,22 @@ class TestFixtureUsages:
"*1 error*" "*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): def test_funcarg_parametrized_and_used_twice(self, testdir):
testdir.makepyfile(""" testdir.makepyfile("""
import pytest import pytest

View File

@ -96,6 +96,14 @@ class TestMetafunc:
pytest.raises(ValueError, lambda: metafunc.parametrize("y", [5,6])) pytest.raises(ValueError, lambda: metafunc.parametrize("y", [5,6]))
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 test_parametrize_and_id(self):
def func(x, y): pass def func(x, y): pass
metafunc = self.Metafunc(func) metafunc = self.Metafunc(func)
@ -105,6 +113,14 @@ class TestMetafunc:
ids = [x.id for x in metafunc._calls] ids = [x.id for x in metafunc._calls]
assert ids == ["basic-abc", "basic-def", "advanced-abc", "advanced-def"] 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 test_parametrize_with_wrong_number_of_ids(self, testdir):
def func(x, y): pass def func(x, y): pass
metafunc = self.Metafunc(func) metafunc = self.Metafunc(func)

View File

@ -108,7 +108,8 @@ class TestImportHookInstallation:
assert result.ret == 0 assert result.ret == 0
@pytest.mark.parametrize('mode', ['plain', 'rewrite']) @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 # Make sure the hook is installed early enough so that plugins
# installed via setuptools are re-written. # installed via setuptools are re-written.
testdir.tmpdir.join('hampkg').ensure(dir=1) testdir.tmpdir.join('hampkg').ensure(dir=1)
@ -135,13 +136,22 @@ class TestImportHookInstallation:
'mainwrapper.py': """ 'mainwrapper.py': """
import pytest, pkg_resources import pytest, pkg_resources
plugin_state = "{plugin_state}"
class DummyDistInfo: class DummyDistInfo:
project_name = 'spam' project_name = 'spam'
version = '1.0' version = '1.0'
def _get_metadata(self, name): def _get_metadata(self, name):
return ['spamplugin.py,sha256=abc,123', # 'RECORD' meta-data only available in installed plugins
'hampkg/__init__.py,sha256=abc,123'] 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: class DummyEntryPoint:
name = 'spam' name = 'spam'
@ -159,7 +169,7 @@ class TestImportHookInstallation:
pkg_resources.iter_entry_points = iter_entry_points pkg_resources.iter_entry_points = iter_entry_points
pytest.main() pytest.main()
""", """.format(plugin_state=plugin_state),
'test_foo.py': """ 'test_foo.py': """
def test(check_first): def test(check_first):
check_first([10, 30], 30) check_first([10, 30], 30)
@ -862,3 +872,15 @@ def test_assert_with_unicode(monkeypatch, testdir):
""") """)
result = testdir.runpytest() result = testdir.runpytest()
result.stdout.fnmatch_lines(['*AssertionError*']) 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()

11
tox.ini
View File

@ -10,8 +10,8 @@ envlist=
[testenv] [testenv]
commands= pytest --lsof -rfsxX {posargs:testing} commands= pytest --lsof -rfsxX {posargs:testing}
passenv = USER USERNAME passenv = USER USERNAME
deps= deps=
hypothesis hypothesis>=3.5.2
nose nose
mock mock
requests requests
@ -47,7 +47,7 @@ commands = flake8 pytest.py _pytest testing
deps=pytest-xdist>=1.13 deps=pytest-xdist>=1.13
mock mock
nose nose
hypothesis hypothesis>=3.5.2
commands= commands=
pytest -n1 -rfsxX {posargs:testing} pytest -n1 -rfsxX {posargs:testing}
@ -71,8 +71,9 @@ commands=
pytest -rfsxX test_pdb.py test_terminal.py test_unittest.py pytest -rfsxX test_pdb.py test_terminal.py test_unittest.py
[testenv:py27-nobyte] [testenv:py27-nobyte]
deps=pytest-xdist>=1.13 deps=
hypothesis pytest-xdist>=1.13
hypothesis>=3.5.2
distribute=true distribute=true
setenv= setenv=
PYTHONDONTWRITEBYTECODE=1 PYTHONDONTWRITEBYTECODE=1