Merge master into features

Conflicts:
	tox.ini
This commit is contained in:
Daniel Hahler 2019-02-13 17:58:16 +01:00
commit 7b91952645
29 changed files with 373 additions and 67 deletions

3
.gitignore vendored
View File

@ -45,3 +45,6 @@ coverage.xml
.project
.settings
.vscode
# generated by pip
pip-wheel-metadata/

View File

@ -17,10 +17,10 @@ env:
# Specialized factors for py27.
- TOXENV=py27-nobyte
- TOXENV=py27-xdist
- TOXENV=py27-pluggymaster PYTEST_NO_COVERAGE=1
- TOXENV=py27-pluggymaster
# Specialized factors for py37.
- TOXENV=py37-pexpect,py37-trial,py37-numpy
- TOXENV=py37-pluggymaster PYTEST_NO_COVERAGE=1
- TOXENV=py37-pluggymaster
- TOXENV=py37-freeze PYTEST_NO_COVERAGE=1
matrix:

View File

@ -18,6 +18,62 @@ with advance notice in the **Deprecations** section of releases.
.. towncrier release notes start
pytest 4.2.1 (2019-02-12)
=========================
Bug Fixes
---------
- `#2895 <https://github.com/pytest-dev/pytest/issues/2895>`_: The ``pytest_report_collectionfinish`` hook now is also called with ``--collect-only``.
- `#3899 <https://github.com/pytest-dev/pytest/issues/3899>`_: Do not raise ``UsageError`` when an imported package has a ``pytest_plugins.py`` child module.
- `#4347 <https://github.com/pytest-dev/pytest/issues/4347>`_: Fix output capturing when using pdb++ with recursive debugging.
- `#4592 <https://github.com/pytest-dev/pytest/issues/4592>`_: Fix handling of ``collect_ignore`` via parent ``conftest.py``.
- `#4700 <https://github.com/pytest-dev/pytest/issues/4700>`_: Fix regression where ``setUpClass`` would always be called in subclasses even if all tests
were skipped by a ``unittest.skip()`` decorator applied in the subclass.
- `#4739 <https://github.com/pytest-dev/pytest/issues/4739>`_: Fix ``parametrize(... ids=<function>)`` when the function returns non-strings.
- `#4745 <https://github.com/pytest-dev/pytest/issues/4745>`_: Fix/improve collection of args when passing in ``__init__.py`` and a test file.
- `#4770 <https://github.com/pytest-dev/pytest/issues/4770>`_: ``more_itertools`` is now constrained to <6.0.0 when required for Python 2.7 compatibility.
- `#526 <https://github.com/pytest-dev/pytest/issues/526>`_: Fix "ValueError: Plugin already registered" exceptions when running in build directories that symlink to actual source.
Improved Documentation
----------------------
- `#3899 <https://github.com/pytest-dev/pytest/issues/3899>`_: Add note to ``plugins.rst`` that ``pytest_plugins`` should not be used as a name for a user module containing plugins.
- `#4324 <https://github.com/pytest-dev/pytest/issues/4324>`_: Document how to use ``raises`` and ``does_not_raise`` to write parametrized tests with conditional raises.
- `#4709 <https://github.com/pytest-dev/pytest/issues/4709>`_: Document how to customize test failure messages when using
``pytest.warns``.
Trivial/Internal Changes
------------------------
- `#4741 <https://github.com/pytest-dev/pytest/issues/4741>`_: Some verbosity related attributes of the TerminalReporter plugin are now
read only properties.
pytest 4.2.0 (2019-01-30)
=========================

View File

@ -11,11 +11,9 @@ environment:
# Specialized factors for py27.
- TOXENV: "py27-trial,py27-numpy,py27-nobyte"
- TOXENV: "py27-pluggymaster"
PYTEST_NO_COVERAGE: "1"
# Specialized factors for py37.
- TOXENV: "py37-trial,py37-numpy"
- TOXENV: "py37-pluggymaster"
PYTEST_NO_COVERAGE: "1"
- TOXENV: "py37-freeze"
PYTEST_NO_COVERAGE: "1"

45
azure-pipelines.yml Normal file
View File

@ -0,0 +1,45 @@
trigger:
- master
- features
variables:
PYTEST_ADDOPTS: "--junitxml=build/test-results/$(tox.env).xml"
jobs:
- job: 'Test'
pool:
vmImage: "vs2017-win2016"
strategy:
matrix:
py27:
python.version: '2.7'
tox.env: 'py27'
py35:
python.version: '3.5'
tox.env: 'py35'
py36:
python.version: '3.6'
tox.env: 'py36'
py37:
python.version: '3.7'
tox.env: 'py37'
maxParallel: 4
steps:
- task: UsePythonVersion@0
inputs:
versionSpec: '$(python.version)'
architecture: 'x64'
- script: python -m pip install --upgrade pip && pip install tox
displayName: 'Install tox'
- script: python -m tox -e $(tox.env)
displayName: 'Run tests'
- task: PublishTestResults@2
inputs:
testResultsFiles: 'build/test-results/$(tox.env).xml'
testRunTitle: '$(tox.env)'
condition: succeededOrFailed()

View File

@ -1 +0,0 @@
The ``pytest_report_collectionfinish`` hook now is also called with ``--collect-only``.

View File

@ -1 +0,0 @@
Do not raise ``UsageError`` when an imported package has a ``pytest_plugins.py`` child module.

View File

@ -1 +0,0 @@
Add note to ``plugins.rst`` that ``pytest_plugins`` should not be used as a name for a user module containing plugins.

View File

@ -1 +0,0 @@
Document how to use ``raises`` and ``does_not_raise`` to write parametrized tests with conditional raises.

View File

@ -1 +0,0 @@
Fix handling of ``collect_ignore`` via parent ``conftest.py``.

View File

@ -1,2 +0,0 @@
Fix regression where ``setUpClass`` would always be called in subclasses even if all tests
were skipped by a ``unittest.skip()`` decorator applied in the subclass.

View File

@ -1,2 +0,0 @@
Document how to customize test failure messages when using
``pytest.warns``.

View File

@ -1 +0,0 @@
Fix ``parametrize(... ids=<function>)`` when the function returns non-strings.

View File

@ -6,6 +6,7 @@ Release announcements
:maxdepth: 2
release-4.2.1
release-4.2.0
release-4.1.1
release-4.1.0

View File

@ -0,0 +1,30 @@
pytest-4.2.1
=======================================
pytest 4.2.1 has just been released to PyPI.
This is a bug-fix release, being a drop-in replacement. To upgrade::
pip install --upgrade pytest
The full changelog is available at https://docs.pytest.org/en/latest/changelog.html.
Thanks to all who contributed to this release, among them:
* Anthony Sottile
* Arel Cordero
* Bruno Oliveira
* Daniel Hahler
* Holger Kohr
* Kevin J. Foley
* Nick Murphy
* Paweł Stradomski
* Raphael Pierzina
* Ronny Pfannschmidt
* Sam Brightman
* Thomas Hisch
* Zac Hatfield-Dodds
Happy testing,
The pytest Development Team

View File

@ -205,8 +205,8 @@ Special comparisons are done for a number of cases:
See the :ref:`reporting demo <tbreportdemo>` for many more examples.
Defining your own assertion comparison
----------------------------------------------
Defining your own explanation for failed assertions
---------------------------------------------------
It is possible to add your own detailed explanations by implementing
the ``pytest_assertrepr_compare`` hook.

View File

@ -1042,6 +1042,20 @@ passed multiple times. The expected format is ``name=value``. For example::
This tells pytest to ignore deprecation warnings and turn all other warnings
into errors. For more information please refer to :ref:`warnings`.
.. confval:: junit_family
.. versionadded:: 4.2
Configures the format of the generated JUnit XML file. The possible options are:
* ``xunit1`` (or ``legacy``): produces old style output, compatible with the xunit 1.0 format. **This is the default**.
* ``xunit2``: produces `xunit 2.0 style output <https://github.com/jenkinsci/xunit-plugin/blob/xunit-2.3.2/src/main/resources/org/jenkinsci/plugins/xunit/types/model/xsd/junit-10.xsd>`__,
which should be more compatible with latest Jenkins versions.
.. code-block:: ini
[pytest]
junit_family = xunit2
.. confval:: junit_suite_name

View File

@ -10,7 +10,8 @@ INSTALL_REQUIRES = [
"six>=1.10.0",
"setuptools",
"attrs>=17.4.0",
"more-itertools>=4.0.0",
'more-itertools>=4.0.0,<6.0.0;python_version<="2.7"',
'more-itertools>=4.0.0;python_version>"2.7"',
"atomicwrites>=1.0",
'funcsigs;python_version<"3.0"',
'pathlib2>=2.2.0;python_version<"3.6"',

View File

@ -408,7 +408,10 @@ class PytestPluginManager(PluginManager):
continue
conftestpath = parent.join("conftest.py")
if conftestpath.isfile():
mod = self._importconftest(conftestpath)
# Use realpath to avoid loading the same conftest twice
# with build systems that create build directories containing
# symlinks to actual files.
mod = self._importconftest(conftestpath.realpath())
clist.append(mod)
self._dirpath2confmods[directory] = clist
return clist

View File

@ -75,6 +75,7 @@ class pytestPDB(object):
_config = None
_pdb_cls = pdb.Pdb
_saved = []
_recursive_debug = 0
@classmethod
def _init_pdb(cls, *args, **kwargs):
@ -87,29 +88,37 @@ class pytestPDB(object):
capman.suspend_global_capture(in_=True)
tw = _pytest.config.create_terminal_writer(cls._config)
tw.line()
# Handle header similar to pdb.set_trace in py37+.
header = kwargs.pop("header", None)
if header is not None:
tw.sep(">", header)
elif capman and capman.is_globally_capturing():
tw.sep(">", "PDB set_trace (IO-capturing turned off)")
else:
tw.sep(">", "PDB set_trace")
if cls._recursive_debug == 0:
# Handle header similar to pdb.set_trace in py37+.
header = kwargs.pop("header", None)
if header is not None:
tw.sep(">", header)
elif capman and capman.is_globally_capturing():
tw.sep(">", "PDB set_trace (IO-capturing turned off)")
else:
tw.sep(">", "PDB set_trace")
class _PdbWrapper(cls._pdb_cls, object):
_pytest_capman = capman
_continued = False
def do_debug(self, arg):
cls._recursive_debug += 1
ret = super(_PdbWrapper, self).do_debug(arg)
cls._recursive_debug -= 1
return ret
def do_continue(self, arg):
ret = super(_PdbWrapper, self).do_continue(arg)
if self._pytest_capman:
tw = _pytest.config.create_terminal_writer(cls._config)
tw.line()
if self._pytest_capman.is_globally_capturing():
tw.sep(">", "PDB continue (IO-capturing resumed)")
else:
tw.sep(">", "PDB continue")
self._pytest_capman.resume_global_capture()
if cls._recursive_debug == 0:
if self._pytest_capman.is_globally_capturing():
tw.sep(">", "PDB continue (IO-capturing resumed)")
else:
tw.sep(">", "PDB continue")
self._pytest_capman.resume_global_capture()
cls._pluginmanager.hook.pytest_leave_pdb(
config=cls._config, pdb=self
)

View File

@ -4,6 +4,7 @@ from __future__ import print_function
import functools
import inspect
import itertools
import sys
import warnings
from collections import defaultdict
@ -13,7 +14,6 @@ from collections import OrderedDict
import attr
import py
import six
from more_itertools import flatten
import _pytest
from _pytest import nodes
@ -1109,7 +1109,7 @@ class FixtureManager(object):
argnames = getfuncargnames(func, cls=cls)
else:
argnames = ()
usefixtures = flatten(
usefixtures = itertools.chain.from_iterable(
mark.args for mark in node.iter_markers(name="usefixtures")
)
initialnames = tuple(usefixtures) + argnames

View File

@ -435,13 +435,6 @@ class LoggingPlugin(object):
# terminal reporter is disabled e.g. by pytest-xdist.
return
# FIXME don't set verbosity level and derived attributes of
# terminalwriter directly
terminal_reporter.verbosity = config.option.verbose
terminal_reporter.showheader = terminal_reporter.verbosity >= 0
terminal_reporter.showfspath = terminal_reporter.verbosity >= 0
terminal_reporter.showlongtestinfo = terminal_reporter.verbosity > 0
capture_manager = config.pluginmanager.get_plugin("capturemanager")
# if capturemanager plugin is disabled, live logging still works.
log_cli_handler = _LiveLoggingStreamHandler(terminal_reporter, capture_manager)

View File

@ -571,19 +571,9 @@ class Session(nodes.FSCollector):
if argpath.check(dir=1):
assert not names, "invalid arg %r" % (arg,)
if six.PY2:
def filter_(f):
return f.check(file=1) and not f.strpath.endswith("*.pyc")
else:
def filter_(f):
return f.check(file=1)
seen_dirs = set()
for path in argpath.visit(
fil=filter_, rec=self._recurse, bf=True, sort=True
fil=self._visit_filter, rec=self._recurse, bf=True, sort=True
):
dirpath = path.dirpath()
if dirpath not in seen_dirs:
@ -613,7 +603,7 @@ class Session(nodes.FSCollector):
col = self._node_cache[argpath]
else:
collect_root = self._pkg_roots.get(argpath.dirname, self)
col = collect_root._collectfile(argpath)
col = collect_root._collectfile(argpath, handle_dupes=False)
if col:
self._node_cache[argpath] = col
m = self.matchnodes(col, names)
@ -658,6 +648,18 @@ class Session(nodes.FSCollector):
ihook.pytest_collect_directory(path=dirpath, parent=self)
return True
if six.PY2:
@staticmethod
def _visit_filter(f):
return f.check(file=1) and not f.strpath.endswith("*.pyc")
else:
@staticmethod
def _visit_filter(f):
return f.check(file=1)
def _tryconvertpyarg(self, x):
"""Convert a dotted module name to path."""
try:

View File

@ -81,7 +81,11 @@ class LsofFdLeakChecker(object):
def _exec_lsof(self):
pid = os.getpid()
return subprocess.check_output(("lsof", "-Ffn0", "-p", str(pid))).decode()
# py3: use subprocess.DEVNULL directly.
with open(os.devnull, "wb") as devnull:
return subprocess.check_output(
("lsof", "-Ffn0", "-p", str(pid)), stderr=devnull
).decode()
def _parse_lsof_output(self, out):
def isopen(line):

View File

@ -222,12 +222,9 @@ class TerminalReporter(object):
import _pytest.config
self.config = config
self.verbosity = self.config.option.verbose
self.showheader = self.verbosity >= 0
self.showfspath = self.verbosity >= 0
self.showlongtestinfo = self.verbosity > 0
self._numcollected = 0
self._session = None
self._showfspath = None
self.stats = {}
self.startdir = py.path.local()
@ -255,6 +252,28 @@ class TerminalReporter(object):
return False
return self.config.getini("console_output_style") in ("progress", "count")
@property
def verbosity(self):
return self.config.option.verbose
@property
def showheader(self):
return self.verbosity >= 0
@property
def showfspath(self):
if self._showfspath is None:
return self.verbosity >= 0
return self._showfspath
@showfspath.setter
def showfspath(self, value):
self._showfspath = value
@property
def showlongtestinfo(self):
return self.verbosity > 0
def hasopt(self, char):
char = {"xfailed": "x", "skipped": "s"}.get(char, char)
return char in self.reportchars

View File

@ -1177,3 +1177,32 @@ def test_collectignore_via_conftest(testdir, monkeypatch):
result = testdir.runpytest()
assert result.ret == EXIT_NOTESTSCOLLECTED
def test_collect_pkg_init_and_file_in_args(testdir):
subdir = testdir.mkdir("sub")
init = subdir.ensure("__init__.py")
init.write("def test_init(): pass")
p = subdir.ensure("test_file.py")
p.write("def test_file(): pass")
# NOTE: without "-o python_files=*.py" this collects test_file.py twice.
# This changed/broke with "Add package scoped fixtures #2283" (2b1410895)
# initially (causing a RecursionError).
result = testdir.runpytest("-v", str(init), str(p))
result.stdout.fnmatch_lines(
[
"sub/test_file.py::test_file PASSED*",
"sub/test_file.py::test_file PASSED*",
"*2 passed in*",
]
)
result = testdir.runpytest("-v", "-o", "python_files=*.py", str(init), str(p))
result.stdout.fnmatch_lines(
[
"sub/__init__.py::test_init PASSED*",
"sub/test_file.py::test_file PASSED*",
"*2 passed in*",
]
)

View File

@ -244,6 +244,42 @@ def test_conftest_symlink(testdir):
assert result.ret == EXIT_OK
@pytest.mark.skipif(
not hasattr(py.path.local, "mksymlinkto"),
reason="symlink not available on this platform",
)
def test_conftest_symlink_files(testdir):
"""Check conftest.py loading when running in directory with symlinks."""
real = testdir.tmpdir.mkdir("real")
source = {
"app/test_foo.py": "def test1(fixture): pass",
"app/__init__.py": "",
"app/conftest.py": textwrap.dedent(
"""
import pytest
print("conftest_loaded")
@pytest.fixture
def fixture():
print("fixture_used")
"""
),
}
testdir.makepyfile(**{"real/%s" % k: v for k, v in source.items()})
# Create a build directory that contains symlinks to actual files
# but doesn't symlink actual directories.
build = testdir.tmpdir.mkdir("build")
build.mkdir("app")
for f in source:
build.join(f).mksymlinkto(real.join(f))
build.chdir()
result = testdir.runpytest("-vs", "app/test_foo.py")
result.stdout.fnmatch_lines(["*conftest_loaded*", "PASSED"])
assert result.ret == EXIT_OK
def test_no_conftest(testdir):
testdir.makeconftest("assert 0")
result = testdir.runpytest("--noconftest")

View File

@ -513,6 +513,76 @@ class TestPDB(object):
assert "1 failed" in rest
self.flush(child)
def test_pdb_interaction_continue_recursive(self, testdir):
p1 = testdir.makepyfile(
mytest="""
import pdb
import pytest
count_continue = 0
# Simulates pdbpp, which injects Pdb into do_debug, and uses
# self.__class__ in do_continue.
class CustomPdb(pdb.Pdb, object):
def do_debug(self, arg):
import sys
import types
newglobals = {
'Pdb': self.__class__, # NOTE: different with pdb.Pdb
'sys': sys,
}
if sys.version_info < (3, ):
do_debug_func = pdb.Pdb.do_debug.im_func
else:
do_debug_func = pdb.Pdb.do_debug
orig_do_debug = types.FunctionType(
do_debug_func.__code__, newglobals,
do_debug_func.__name__, do_debug_func.__defaults__,
)
return orig_do_debug(self, arg)
do_debug.__doc__ = pdb.Pdb.do_debug.__doc__
def do_continue(self, *args, **kwargs):
global count_continue
count_continue += 1
return super(CustomPdb, self).do_continue(*args, **kwargs)
def foo():
print("print_from_foo")
def test_1():
i = 0
print("hello17")
pytest.set_trace()
x = 3
print("hello18")
assert count_continue == 2, "unexpected_failure: %d != 2" % count_continue
pytest.fail("expected_failure")
"""
)
child = testdir.spawn_pytest("--pdbcls=mytest:CustomPdb %s" % str(p1))
child.expect(r"PDB set_trace \(IO-capturing turned off\)")
child.expect(r"\n\(Pdb")
child.sendline("debug foo()")
child.expect("ENTERING RECURSIVE DEBUGGER")
child.expect(r"\n\(\(Pdb")
child.sendline("c")
child.expect("LEAVING RECURSIVE DEBUGGER")
assert b"PDB continue" not in child.before
assert b"print_from_foo" in child.before
child.sendline("c")
child.expect(r"PDB continue \(IO-capturing resumed\)")
rest = child.read().decode("utf8")
assert "hello17" in rest # out is captured
assert "hello18" in rest # out is captured
assert "1 failed" in rest
assert "Failed: expected_failure" in rest
assert "AssertionError: unexpected_failure" not in rest
self.flush(child)
def test_pdb_without_capture(self, testdir):
p1 = testdir.makepyfile(
"""

23
tox.ini
View File

@ -1,6 +1,6 @@
[tox]
isolated_build = True
minversion = 3.3
minversion = 3.5.3
distshare = {homedir}/.tox/distshare
# make sure to update environment list in travis.yml and appveyor.yml
envlist =
@ -23,10 +23,11 @@ commands =
{env:_PYTEST_TOX_COVERAGE_RUN:} pytest {env:_PYTEST_TOX_ARGS:} {posargs}
coverage: coverage combine
coverage: coverage report
passenv = USER USERNAME COVERAGE_* TRAVIS
passenv = USER USERNAME COVERAGE_* TRAVIS PYTEST_ADDOPTS
setenv =
_PYTEST_TOX_ARGS=--lsof
# configuration if a user runs tox with a "coverage" factor, for example "tox -e py37-coverage"
# Configuration to run with coverage similar to Travis/Appveyor, e.g.
# "tox -e py37-coverage".
coverage: _PYTEST_TOX_COVERAGE_RUN=coverage run -m
coverage: _PYTEST_TOX_EXTRA_DEP=coverage-enable-subprocess
coverage: COVERAGE_FILE={toxinidir}/.coverage
@ -55,8 +56,8 @@ commands = pre-commit run --all-files --show-diff-on-failure
[testenv:py27-pexpect]
platform = linux|darwin
deps =
{[testenv]deps}
pexpect
{env:_PYTEST_TOX_EXTRA_DEP:}
commands =
{env:_PYTEST_TOX_COVERAGE_RUN:} pytest {posargs:testing/test_pdb.py testing/test_terminal.py testing/test_unittest.py}
@ -68,8 +69,8 @@ commands = {[testenv:py27-pexpect]commands}
[testenv:py27-nobyte]
extras = testing
deps =
{[testenv]deps}
pytest-xdist>=1.13
{env:_PYTEST_TOX_EXTRA_DEP:}
distribute = true
setenv =
{[testenv]setenv}
@ -79,8 +80,8 @@ commands =
[testenv:py27-trial]
deps =
{[testenv]deps}
twisted
{env:_PYTEST_TOX_EXTRA_DEP:}
commands =
{env:_PYTEST_TOX_COVERAGE_RUN:} pytest {posargs:testing/test_unittest.py}
@ -90,8 +91,8 @@ commands = {[testenv:py27-trial]commands}
[testenv:py27-numpy]
deps =
{[testenv]deps}
numpy
{env:_PYTEST_TOX_EXTRA_DEP:}
commands=
{env:_PYTEST_TOX_COVERAGE_RUN:} pytest {posargs:testing/python/approx.py}
@ -103,11 +104,13 @@ commands = {[testenv:py27-numpy]commands}
setenv=
{[testenv]setenv}
_PYTEST_SETUP_SKIP_PLUGGY_DEP=1
# NOTE: using env instead of "{[testenv]deps}", because of https://github.com/tox-dev/tox/issues/706.
_PYTEST_TOX_EXTRA_DEP=git+https://github.com/pytest-dev/pluggy.git@master
deps =
{[testenv]deps}
git+https://github.com/pytest-dev/pluggy.git@master
[testenv:py37-pluggymaster]
setenv = {[testenv:py27-pluggymaster]setenv}
deps = {[testenv:py27-pluggymaster]deps}
[testenv:docs]
basepython = python3
@ -123,8 +126,8 @@ commands =
basepython = python3
skipsdist = True
deps =
{[testenv]deps}
PyYAML
{env:_PYTEST_TOX_EXTRA_DEP:}
commands =
{env:_PYTEST_TOX_COVERAGE_RUN:} pytest doc/en
{env:_PYTEST_TOX_COVERAGE_RUN:} pytest --doctest-modules --pyargs _pytest