From 62db3f7abcec8cfdcd8526310f866d0fd08d8025 Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Tue, 21 Jan 2020 19:27:57 +0100 Subject: [PATCH 01/23] typing: fix/adjust _code.source.getfslineno --- src/_pytest/_code/source.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/_pytest/_code/source.py b/src/_pytest/_code/source.py index 67c74143f..379393b10 100644 --- a/src/_pytest/_code/source.py +++ b/src/_pytest/_code/source.py @@ -282,7 +282,7 @@ def compile_( # noqa: F811 return s.compile(filename, mode, flags, _genframe=_genframe) -def getfslineno(obj) -> Tuple[Union[str, py.path.local], int]: +def getfslineno(obj) -> Tuple[Optional[Union["Literal['']", py.path.local]], int]: """ Return source location (path, lineno) for the given object. If the source cannot be determined return ("", -1). From bd6ba3f3e12d61c8011ff0b77d38a2221ddfbbc3 Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Tue, 21 Jan 2020 15:08:53 +0100 Subject: [PATCH 02/23] typing: Session.__init__ Pulled out of https://github.com/pytest-dev/pytest/pull/6491. --- src/_pytest/main.py | 21 ++++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/src/_pytest/main.py b/src/_pytest/main.py index a0f180cac..5137713d9 100644 --- a/src/_pytest/main.py +++ b/src/_pytest/main.py @@ -6,21 +6,30 @@ import importlib import os import sys from typing import Dict +from typing import FrozenSet +from typing import List import attr import py import _pytest._code from _pytest import nodes +from _pytest.compat import TYPE_CHECKING +from _pytest.config import Config from _pytest.config import directory_arg from _pytest.config import hookimpl from _pytest.config import UsageError from _pytest.fixtures import FixtureManager +from _pytest.nodes import Node from _pytest.outcomes import exit from _pytest.runner import collect_one_node from _pytest.runner import SetupState +if TYPE_CHECKING: + from _pytest.python import Package + + class ExitCode(enum.IntEnum): """ .. versionadded:: 5.0 @@ -383,7 +392,7 @@ class Session(nodes.FSCollector): # Set on the session by fixtures.pytest_sessionstart. _fixturemanager = None # type: FixtureManager - def __init__(self, config): + def __init__(self, config: Config) -> None: nodes.FSCollector.__init__( self, config.rootdir, parent=None, config=config, session=self, nodeid="" ) @@ -394,14 +403,16 @@ class Session(nodes.FSCollector): self.trace = config.trace.root.get("collection") self._norecursepatterns = config.getini("norecursedirs") self.startdir = config.invocation_dir - self._initialpaths = frozenset() + self._initialpaths = frozenset() # type: FrozenSet[py.path.local] + # Keep track of any collected nodes in here, so we don't duplicate fixtures - self._node_cache = {} + self._node_cache = {} # type: Dict[str, List[Node]] + # Dirnames of pkgs with dunder-init files. + self._pkg_roots = {} # type: Dict[py.path.local, Package] + self._bestrelpathcache = _bestrelpath_cache( config.rootdir ) # type: Dict[str, str] - # Dirnames of pkgs with dunder-init files. - self._pkg_roots = {} self.config.pluginmanager.register(self, name="session") From 8fa57c8384818efcc83af604f0ccfc2620dd111c Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Tue, 21 Jan 2020 11:30:32 +0100 Subject: [PATCH 03/23] tests: improve test for `nose.raises` This should probably get transferred into a `pytest.fail` really, but tests/documents the current behavior. --- testing/test_nose.py | 36 +++++++++++++++++++++++++++++++++--- 1 file changed, 33 insertions(+), 3 deletions(-) diff --git a/testing/test_nose.py b/testing/test_nose.py index 469c127af..15be7b73a 100644 --- a/testing/test_nose.py +++ b/testing/test_nose.py @@ -377,15 +377,45 @@ def test_skip_test_with_unicode(testdir): result.stdout.fnmatch_lines(["* 1 skipped *"]) -def test_issue_6517(testdir): +def test_raises(testdir): testdir.makepyfile( """ from nose.tools import raises @raises(RuntimeError) - def test_fail_without_tcp(): + def test_raises_runtimeerror(): raise RuntimeError + + @raises(Exception) + def test_raises_baseexception_not_caught(): + raise BaseException + + @raises(BaseException) + def test_raises_baseexception_caught(): + raise BaseException """ ) result = testdir.runpytest() - result.stdout.fnmatch_lines(["* 1 passed *"]) + result.stdout.fnmatch_lines( + [ + "*= FAILURES =*", + "*_ test_raises_baseexception_not_caught _*", + "", + "arg = (), kw = {}", + "", + " def newfunc(*arg, **kw):", + " try:", + "> func(*arg, **kw)", + "", + "*/nose/*: ", + "_ _ *", + "", + " @raises(Exception)", + " def test_raises_baseexception_not_caught():", + "> raise BaseException", + "E BaseException", + "", + "test_raises.py:9: BaseException", + "* 1 failed, 2 passed *", + ] + ) From ef112fd7dd8ee220865f7569c0ecc4fa5eb24464 Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Tue, 21 Jan 2020 12:29:22 +0100 Subject: [PATCH 04/23] Revert "Revert "Fix type errors after adding types to the `py` dependency"" Without changes to test_itemreport_reportinfo. This reverts commit fb99b5c66ee06ad0bd3336d8599448d1d3da4f7f. Conflicts: testing/test_nose.py --- src/_pytest/config/argparsing.py | 8 ++++---- src/_pytest/config/findpaths.py | 7 +++++-- src/_pytest/doctest.py | 2 +- src/_pytest/fixtures.py | 11 +++++++---- src/_pytest/main.py | 8 ++++---- src/_pytest/nodes.py | 1 + 6 files changed, 22 insertions(+), 15 deletions(-) diff --git a/src/_pytest/config/argparsing.py b/src/_pytest/config/argparsing.py index 7cbb676bd..8817c5749 100644 --- a/src/_pytest/config/argparsing.py +++ b/src/_pytest/config/argparsing.py @@ -82,8 +82,8 @@ class Parser: self.optparser = self._getparser() try_argcomplete(self.optparser) - args = [str(x) if isinstance(x, py.path.local) else x for x in args] - return self.optparser.parse_args(args, namespace=namespace) + strargs = [str(x) if isinstance(x, py.path.local) else x for x in args] + return self.optparser.parse_args(strargs, namespace=namespace) def _getparser(self) -> "MyOptionParser": from _pytest._argcomplete import filescompleter @@ -124,8 +124,8 @@ class Parser: the remaining arguments unknown at this point. """ optparser = self._getparser() - args = [str(x) if isinstance(x, py.path.local) else x for x in args] - return optparser.parse_known_args(args, namespace=namespace) + strargs = [str(x) if isinstance(x, py.path.local) else x for x in args] + return optparser.parse_known_args(strargs, namespace=namespace) def addini(self, name, help, type=None, default=None): """ register an ini-file option. diff --git a/src/_pytest/config/findpaths.py b/src/_pytest/config/findpaths.py index 707ce969d..fb84160c1 100644 --- a/src/_pytest/config/findpaths.py +++ b/src/_pytest/config/findpaths.py @@ -1,6 +1,9 @@ import os +from typing import Any +from typing import Iterable from typing import List from typing import Optional +from typing import Tuple import py @@ -60,7 +63,7 @@ def getcfg(args, config=None): return None, None, None -def get_common_ancestor(paths): +def get_common_ancestor(paths: Iterable[py.path.local]) -> py.path.local: common_ancestor = None for path in paths: if not path.exists(): @@ -113,7 +116,7 @@ def determine_setup( args: List[str], rootdir_cmd_arg: Optional[str] = None, config: Optional["Config"] = None, -): +) -> Tuple[py.path.local, Optional[str], Any]: dirs = get_dirs_from_args(args) if inifile: iniconfig = py.iniconfig.IniConfig(inifile) diff --git a/src/_pytest/doctest.py b/src/_pytest/doctest.py index e62c5e17e..d7ca888cc 100644 --- a/src/_pytest/doctest.py +++ b/src/_pytest/doctest.py @@ -308,7 +308,7 @@ class DoctestItem(pytest.Item): else: return super().repr_failure(excinfo) - def reportinfo(self) -> Tuple[str, int, str]: + def reportinfo(self) -> Tuple[py.path.local, int, str]: return self.fspath, self.dtest.lineno, "[doctest] %s" % self.name diff --git a/src/_pytest/fixtures.py b/src/_pytest/fixtures.py index bae3d0716..f0a1a2ed0 100644 --- a/src/_pytest/fixtures.py +++ b/src/_pytest/fixtures.py @@ -351,7 +351,7 @@ class FixtureRequest: self.fixturename = None #: Scope string, one of "function", "class", "module", "session" self.scope = "function" - self._fixture_defs = {} # argname -> FixtureDef + self._fixture_defs = {} # type: Dict[str, FixtureDef] fixtureinfo = pyfuncitem._fixtureinfo self._arg2fixturedefs = fixtureinfo.name2fixturedefs.copy() self._arg2index = {} @@ -426,7 +426,8 @@ class FixtureRequest: @scopeproperty() def fspath(self) -> py.path.local: """ the file system path of the test module which collected this test. """ - return self._pyfuncitem.fspath + # TODO: Remove ignore once _pyfuncitem is properly typed. + return self._pyfuncitem.fspath # type: ignore @property def keywords(self): @@ -549,7 +550,9 @@ class FixtureRequest: source_lineno = frameinfo.lineno source_path = py.path.local(source_path) if source_path.relto(funcitem.config.rootdir): - source_path = source_path.relto(funcitem.config.rootdir) + source_path_str = source_path.relto(funcitem.config.rootdir) + else: + source_path_str = str(source_path) msg = ( "The requested fixture has no parameter defined for test:\n" " {}\n\n" @@ -558,7 +561,7 @@ class FixtureRequest: funcitem.nodeid, fixturedef.argname, getlocation(fixturedef.func, funcitem.config.rootdir), - source_path, + source_path_str, source_lineno, ) ) diff --git a/src/_pytest/main.py b/src/_pytest/main.py index 5137713d9..ae115b1f8 100644 --- a/src/_pytest/main.py +++ b/src/_pytest/main.py @@ -376,9 +376,9 @@ class Failed(Exception): @attr.s class _bestrelpath_cache(dict): - path = attr.ib() + path = attr.ib(type=py.path.local) - def __missing__(self, path: str) -> str: + def __missing__(self, path: py.path.local) -> str: r = self.path.bestrelpath(path) # type: str self[path] = r return r @@ -412,7 +412,7 @@ class Session(nodes.FSCollector): self._bestrelpathcache = _bestrelpath_cache( config.rootdir - ) # type: Dict[str, str] + ) # type: Dict[py.path.local, str] self.config.pluginmanager.register(self, name="session") @@ -425,7 +425,7 @@ class Session(nodes.FSCollector): self.testscollected, ) - def _node_location_to_relpath(self, node_path: str) -> str: + def _node_location_to_relpath(self, node_path: py.path.local) -> str: # bestrelpath is a quite slow function return self._bestrelpathcache[node_path] diff --git a/src/_pytest/nodes.py b/src/_pytest/nodes.py index 3cfbf4626..fc951d2bc 100644 --- a/src/_pytest/nodes.py +++ b/src/_pytest/nodes.py @@ -462,6 +462,7 @@ class Item(Node): @cached_property def location(self) -> Tuple[str, Optional[int], str]: location = self.reportinfo() + assert isinstance(location[0], py.path.local), location[0] fspath = self.session._node_location_to_relpath(location[0]) assert type(location[2]) is str return (fspath, location[1], location[2]) From 1350c601dcf013ae773233206ac1912093dddad9 Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Tue, 21 Jan 2020 12:26:29 +0100 Subject: [PATCH 05/23] Node.location: handle str with _node_location_to_relpath --- src/_pytest/nodes.py | 6 ++++-- testing/test_nose.py | 5 ++++- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/src/_pytest/nodes.py b/src/_pytest/nodes.py index fc951d2bc..080f079cd 100644 --- a/src/_pytest/nodes.py +++ b/src/_pytest/nodes.py @@ -462,7 +462,9 @@ class Item(Node): @cached_property def location(self) -> Tuple[str, Optional[int], str]: location = self.reportinfo() - assert isinstance(location[0], py.path.local), location[0] - fspath = self.session._node_location_to_relpath(location[0]) + fspath = location[0] + if not isinstance(fspath, py.path.local): + fspath = py.path.local(fspath) + fspath = self.session._node_location_to_relpath(fspath) assert type(location[2]) is str return (fspath, location[1], location[2]) diff --git a/testing/test_nose.py b/testing/test_nose.py index 15be7b73a..b6200c6c9 100644 --- a/testing/test_nose.py +++ b/testing/test_nose.py @@ -395,9 +395,12 @@ def test_raises(testdir): raise BaseException """ ) - result = testdir.runpytest() + result = testdir.runpytest("-vv") result.stdout.fnmatch_lines( [ + "test_raises.py::test_raises_runtimeerror PASSED*", + "test_raises.py::test_raises_baseexception_not_caught FAILED*", + "test_raises.py::test_raises_baseexception_caught PASSED*", "*= FAILURES =*", "*_ test_raises_baseexception_not_caught _*", "", From 9c7b3c57d7d6e965eb135d793bb69ef0041aabf5 Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Tue, 21 Jan 2020 12:51:16 +0100 Subject: [PATCH 06/23] typing: PyobjMixin.reportinfo, getfslineno --- src/_pytest/compat.py | 2 +- src/_pytest/python.py | 10 ++++++---- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/src/_pytest/compat.py b/src/_pytest/compat.py index 6a62e88cf..f0b0d548f 100644 --- a/src/_pytest/compat.py +++ b/src/_pytest/compat.py @@ -307,7 +307,7 @@ def get_real_method(obj, holder): return obj -def getfslineno(obj): +def getfslineno(obj) -> Tuple[Union[str, py.path.local], int]: # xxx let decorators etc specify a sane ordering obj = get_real_func(obj) if hasattr(obj, "place_as"): diff --git a/src/_pytest/python.py b/src/_pytest/python.py index bc35ccf5f..3d2916c83 100644 --- a/src/_pytest/python.py +++ b/src/_pytest/python.py @@ -12,6 +12,7 @@ from functools import partial from textwrap import dedent from typing import List from typing import Tuple +from typing import Union import py @@ -280,15 +281,16 @@ class PyobjMixin(PyobjContext): parts.reverse() return ".".join(parts) - def reportinfo(self) -> Tuple[str, int, str]: + def reportinfo(self) -> Tuple[Union[py.path.local, str], int, str]: # XXX caching? obj = self.obj compat_co_firstlineno = getattr(obj, "compat_co_firstlineno", None) if isinstance(compat_co_firstlineno, int): # nose compatibility - fspath = sys.modules[obj.__module__].__file__ - if fspath.endswith(".pyc"): - fspath = fspath[:-1] + file_path = sys.modules[obj.__module__].__file__ + if file_path.endswith(".pyc"): + file_path = file_path[:-1] + fspath = file_path # type: Union[py.path.local, str] lineno = compat_co_firstlineno else: fspath, lineno = getfslineno(obj) From 252eae5bc810cc4356bdc2d803846f2febda676f Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Thu, 23 Jan 2020 10:39:28 +0100 Subject: [PATCH 07/23] tests: fix/harden test_record_property `-rv` is not a recognized reportchar. Probably `-v` was meant, but is not necessary to check that there are no warnings. Followup to 2018cf12b (https://github.com/pytest-dev/pytest/pull/3360). --- testing/test_junitxml.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/testing/test_junitxml.py b/testing/test_junitxml.py index 0132db59d..f6d90852f 100644 --- a/testing/test_junitxml.py +++ b/testing/test_junitxml.py @@ -1061,13 +1061,14 @@ def test_record_property(testdir, run_and_parse): record_property("foo", "<1"); """ ) - result, dom = run_and_parse("-rwv") + result, dom = run_and_parse("-rw") node = dom.find_first_by_tag("testsuite") tnode = node.find_first_by_tag("testcase") psnode = tnode.find_first_by_tag("properties") pnodes = psnode.find_by_tag("property") pnodes[0].assert_attr(name="bar", value="1") pnodes[1].assert_attr(name="foo", value="<1") + result.stdout.fnmatch_lines(["*= 1 passed in *"]) def test_record_property_same_name(testdir, run_and_parse): From e7444bbd5e04ec753cc29343f4705cb6c097f72a Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Thu, 23 Jan 2020 11:35:35 +0100 Subject: [PATCH 08/23] tests: remove unnecessary `-rw` option Warnings are enabled by default, which is tested by `test_getreportopt`. --- testing/python/collect.py | 10 +++++----- testing/test_assertion.py | 2 +- testing/test_cacheprovider.py | 2 +- testing/test_junitxml.py | 8 ++++---- testing/test_pluginmanager.py | 2 +- 5 files changed, 12 insertions(+), 12 deletions(-) diff --git a/testing/python/collect.py b/testing/python/collect.py index 6e9938227..f38c309b2 100644 --- a/testing/python/collect.py +++ b/testing/python/collect.py @@ -68,7 +68,7 @@ class TestModule: def test_invalid_test_module_name(self, testdir): a = testdir.mkdir("a") a.ensure("test_one.part1.py") - result = testdir.runpytest("-rw") + result = testdir.runpytest() result.stdout.fnmatch_lines( [ "ImportError while importing test module*test_one.part1*", @@ -137,7 +137,7 @@ class TestClass: pass """ ) - result = testdir.runpytest("-rw") + result = testdir.runpytest() result.stdout.fnmatch_lines( [ "*cannot collect test class 'TestClass1' because it has " @@ -153,7 +153,7 @@ class TestClass: pass """ ) - result = testdir.runpytest("-rw") + result = testdir.runpytest() result.stdout.fnmatch_lines( [ "*cannot collect test class 'TestClass1' because it has " @@ -230,7 +230,7 @@ class TestClass: TestCase = collections.namedtuple('TestCase', ['a']) """ ) - result = testdir.runpytest("-rw") + result = testdir.runpytest() result.stdout.fnmatch_lines( "*cannot collect test class 'TestCase' " "because it has a __new__ constructor*" @@ -1162,7 +1162,7 @@ def test_dont_collect_non_function_callable(testdir): pass """ ) - result = testdir.runpytest("-rw") + result = testdir.runpytest() result.stdout.fnmatch_lines( [ "*collected 1 item*", diff --git a/testing/test_assertion.py b/testing/test_assertion.py index 5c0425829..e975a3fea 100644 --- a/testing/test_assertion.py +++ b/testing/test_assertion.py @@ -1349,7 +1349,7 @@ def test_assert_indirect_tuple_no_warning(testdir): assert tpl """ ) - result = testdir.runpytest("-rw") + result = testdir.runpytest() output = "\n".join(result.stdout.lines) assert "WR1" not in output diff --git a/testing/test_cacheprovider.py b/testing/test_cacheprovider.py index e22c1c766..b74be5a73 100644 --- a/testing/test_cacheprovider.py +++ b/testing/test_cacheprovider.py @@ -66,7 +66,7 @@ class TestNewAPI: testdir.tmpdir.ensure_dir(".pytest_cache").chmod(0) try: testdir.makepyfile("def test_error(): raise Exception") - result = testdir.runpytest("-rw") + result = testdir.runpytest() assert result.ret == 1 # warnings from nodeids, lastfailed, and stepwise result.stdout.fnmatch_lines( diff --git a/testing/test_junitxml.py b/testing/test_junitxml.py index f6d90852f..365332d70 100644 --- a/testing/test_junitxml.py +++ b/testing/test_junitxml.py @@ -1061,7 +1061,7 @@ def test_record_property(testdir, run_and_parse): record_property("foo", "<1"); """ ) - result, dom = run_and_parse("-rw") + result, dom = run_and_parse() node = dom.find_first_by_tag("testsuite") tnode = node.find_first_by_tag("testcase") psnode = tnode.find_first_by_tag("properties") @@ -1079,7 +1079,7 @@ def test_record_property_same_name(testdir, run_and_parse): record_property("foo", "baz") """ ) - result, dom = run_and_parse("-rw") + result, dom = run_and_parse() node = dom.find_first_by_tag("testsuite") tnode = node.find_first_by_tag("testcase") psnode = tnode.find_first_by_tag("properties") @@ -1121,7 +1121,7 @@ def test_record_attribute(testdir, run_and_parse): record_xml_attribute("foo", "<1"); """ ) - result, dom = run_and_parse("-rw") + result, dom = run_and_parse() node = dom.find_first_by_tag("testsuite") tnode = node.find_first_by_tag("testcase") tnode.assert_attr(bar="1") @@ -1156,7 +1156,7 @@ def test_record_fixtures_xunit2(testdir, fixture_name, run_and_parse): ) ) - result, dom = run_and_parse("-rw", family=None) + result, dom = run_and_parse(family=None) expected_lines = [] if fixture_name == "record_xml_attribute": expected_lines.append( diff --git a/testing/test_pluginmanager.py b/testing/test_pluginmanager.py index e3402d207..5ddb3e41a 100644 --- a/testing/test_pluginmanager.py +++ b/testing/test_pluginmanager.py @@ -256,7 +256,7 @@ class TestPytestPluginManager: ) p.copy(p.dirpath("skipping2.py")) monkeypatch.setenv("PYTEST_PLUGINS", "skipping2") - result = testdir.runpytest("-rw", "-p", "skipping1", syspathinsert=True) + result = testdir.runpytest("-p", "skipping1", syspathinsert=True) assert result.ret == ExitCode.NO_TESTS_COLLECTED result.stdout.fnmatch_lines( ["*skipped plugin*skipping1*hello*", "*skipped plugin*skipping2*hello*"] From b63cb18776b1c9fe315c6b6123da911839c3947c Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Thu, 23 Jan 2020 12:23:30 +0100 Subject: [PATCH 09/23] doc: remove costlysetup example It is not included with docs, and `example/costlysetup/sub_a/test_quick.py::test_quick` sleeps for 5s, slowing down `doctesting` unnecessarily. --- doc/en/example/costlysetup/conftest.py | 20 ------------------- doc/en/example/costlysetup/sub_a/__init__.py | 1 - .../example/costlysetup/sub_a/test_quick.py | 2 -- doc/en/example/costlysetup/sub_b/__init__.py | 1 - doc/en/example/costlysetup/sub_b/test_two.py | 6 ------ 5 files changed, 30 deletions(-) delete mode 100644 doc/en/example/costlysetup/conftest.py delete mode 100644 doc/en/example/costlysetup/sub_a/__init__.py delete mode 100644 doc/en/example/costlysetup/sub_a/test_quick.py delete mode 100644 doc/en/example/costlysetup/sub_b/__init__.py delete mode 100644 doc/en/example/costlysetup/sub_b/test_two.py diff --git a/doc/en/example/costlysetup/conftest.py b/doc/en/example/costlysetup/conftest.py deleted file mode 100644 index 803559834..000000000 --- a/doc/en/example/costlysetup/conftest.py +++ /dev/null @@ -1,20 +0,0 @@ -import pytest - - -@pytest.fixture(scope="session") -def setup(request): - setup = CostlySetup() - yield setup - setup.finalize() - - -class CostlySetup: - def __init__(self): - import time - - print("performing costly setup") - time.sleep(5) - self.timecostly = 1 - - def finalize(self): - del self.timecostly diff --git a/doc/en/example/costlysetup/sub_a/__init__.py b/doc/en/example/costlysetup/sub_a/__init__.py deleted file mode 100644 index 792d60054..000000000 --- a/doc/en/example/costlysetup/sub_a/__init__.py +++ /dev/null @@ -1 +0,0 @@ -# diff --git a/doc/en/example/costlysetup/sub_a/test_quick.py b/doc/en/example/costlysetup/sub_a/test_quick.py deleted file mode 100644 index 38dda2660..000000000 --- a/doc/en/example/costlysetup/sub_a/test_quick.py +++ /dev/null @@ -1,2 +0,0 @@ -def test_quick(setup): - pass diff --git a/doc/en/example/costlysetup/sub_b/__init__.py b/doc/en/example/costlysetup/sub_b/__init__.py deleted file mode 100644 index 792d60054..000000000 --- a/doc/en/example/costlysetup/sub_b/__init__.py +++ /dev/null @@ -1 +0,0 @@ -# diff --git a/doc/en/example/costlysetup/sub_b/test_two.py b/doc/en/example/costlysetup/sub_b/test_two.py deleted file mode 100644 index b1653aaab..000000000 --- a/doc/en/example/costlysetup/sub_b/test_two.py +++ /dev/null @@ -1,6 +0,0 @@ -def test_something(setup): - assert setup.timecostly == 1 - - -def test_something_more(setup): - assert setup.timecostly == 1 From 0b6258ab5bce985b14f28aa9e7bfe828b1d5b8c3 Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Thu, 23 Jan 2020 12:54:52 +0100 Subject: [PATCH 10/23] PyCollector.collect: use explicit cast to `str` Ref: https://github.com/pytest-dev/pytest/pull/6521#pullrequestreview-347234792 --- src/_pytest/python.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/_pytest/python.py b/src/_pytest/python.py index 3d2916c83..82dca3bcc 100644 --- a/src/_pytest/python.py +++ b/src/_pytest/python.py @@ -369,7 +369,12 @@ class PyCollector(PyobjMixin, nodes.Collector): if not isinstance(res, list): res = [res] values.extend(res) - values.sort(key=lambda item: item.reportinfo()[:2]) + + def sort_key(item): + fspath, lineno, _ = item.reportinfo() + return (str(fspath), lineno) + + values.sort(key=sort_key) return values def _makeitem(self, name, obj): From 9dcdea5de7a9d7f3d24a59182682fa6a0fd59bdb Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Thu, 23 Jan 2020 13:25:15 +0100 Subject: [PATCH 11/23] Rewrite Item.location to be clearer with regard to types --- src/_pytest/nodes.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/src/_pytest/nodes.py b/src/_pytest/nodes.py index 080f079cd..ab976efae 100644 --- a/src/_pytest/nodes.py +++ b/src/_pytest/nodes.py @@ -462,9 +462,10 @@ class Item(Node): @cached_property def location(self) -> Tuple[str, Optional[int], str]: location = self.reportinfo() - fspath = location[0] - if not isinstance(fspath, py.path.local): - fspath = py.path.local(fspath) - fspath = self.session._node_location_to_relpath(fspath) + if isinstance(location[0], py.path.local): + fspath = location[0] + else: + fspath = py.path.local(location[0]) + relfspath = self.session._node_location_to_relpath(fspath) assert type(location[2]) is str - return (fspath, location[1], location[2]) + return (relfspath, location[1], location[2]) From 03bc8aba4e818d79668adf9d0049b92b5f7c100e Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Thu, 23 Jan 2020 12:45:38 +0100 Subject: [PATCH 12/23] config: typing for create_terminal_writer, re-export TerminalWriter This also imports `TerminalWriter` explicitly via `_pytest._io`, allowing for easier extending / replacing it. --- .pre-commit-config.yaml | 2 +- src/_pytest/_code/code.py | 23 ++++++++++++----------- src/_pytest/_io/__init__.py | 3 +++ src/_pytest/cacheprovider.py | 3 ++- src/_pytest/config/__init__.py | 9 +++++---- src/_pytest/doctest.py | 5 +++-- src/_pytest/fixtures.py | 3 ++- src/_pytest/reports.py | 3 ++- testing/code/test_excinfo.py | 7 ++++--- testing/logging/test_formatter.py | 7 +++---- 10 files changed, 37 insertions(+), 28 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 978cfcde8..8894c713f 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -64,7 +64,7 @@ repos: _code\.| builtin\.| code\.| - io\.(BytesIO|saferepr)| + io\.(BytesIO|saferepr|TerminalWriter)| path\.local\.sysfind| process\.| std\. diff --git a/src/_pytest/_code/code.py b/src/_pytest/_code/code.py index 3620970fd..94ad4292e 100644 --- a/src/_pytest/_code/code.py +++ b/src/_pytest/_code/code.py @@ -29,6 +29,7 @@ import pluggy import py import _pytest +from _pytest._io import TerminalWriter from _pytest._io.saferepr import safeformat from _pytest._io.saferepr import saferepr from _pytest.compat import overload @@ -915,14 +916,14 @@ class TerminalRepr: # FYI this is called from pytest-xdist's serialization of exception # information. io = StringIO() - tw = py.io.TerminalWriter(file=io) + tw = TerminalWriter(file=io) self.toterminal(tw) return io.getvalue().strip() def __repr__(self) -> str: return "<{} instance at {:0x}>".format(self.__class__, id(self)) - def toterminal(self, tw: py.io.TerminalWriter) -> None: + def toterminal(self, tw: TerminalWriter) -> None: raise NotImplementedError() @@ -933,7 +934,7 @@ class ExceptionRepr(TerminalRepr): def addsection(self, name: str, content: str, sep: str = "-") -> None: self.sections.append((name, content, sep)) - def toterminal(self, tw: py.io.TerminalWriter) -> None: + def toterminal(self, tw: TerminalWriter) -> None: for name, content, sep in self.sections: tw.sep(sep, name) tw.line(content) @@ -953,7 +954,7 @@ class ExceptionChainRepr(ExceptionRepr): self.reprtraceback = chain[-1][0] self.reprcrash = chain[-1][1] - def toterminal(self, tw: py.io.TerminalWriter) -> None: + def toterminal(self, tw: TerminalWriter) -> None: for element in self.chain: element[0].toterminal(tw) if element[2] is not None: @@ -970,7 +971,7 @@ class ReprExceptionInfo(ExceptionRepr): self.reprtraceback = reprtraceback self.reprcrash = reprcrash - def toterminal(self, tw: py.io.TerminalWriter) -> None: + def toterminal(self, tw: TerminalWriter) -> None: self.reprtraceback.toterminal(tw) super().toterminal(tw) @@ -988,7 +989,7 @@ class ReprTraceback(TerminalRepr): self.extraline = extraline self.style = style - def toterminal(self, tw: py.io.TerminalWriter) -> None: + def toterminal(self, tw: TerminalWriter) -> None: # the entries might have different styles for i, entry in enumerate(self.reprentries): if entry.style == "long": @@ -1020,7 +1021,7 @@ class ReprEntryNative(TerminalRepr): def __init__(self, tblines: Sequence[str]) -> None: self.lines = tblines - def toterminal(self, tw: py.io.TerminalWriter) -> None: + def toterminal(self, tw: TerminalWriter) -> None: tw.write("".join(self.lines)) @@ -1039,7 +1040,7 @@ class ReprEntry(TerminalRepr): self.reprfileloc = filelocrepr self.style = style - def toterminal(self, tw: py.io.TerminalWriter) -> None: + def toterminal(self, tw: TerminalWriter) -> None: if self.style == "short": assert self.reprfileloc is not None self.reprfileloc.toterminal(tw) @@ -1072,7 +1073,7 @@ class ReprFileLocation(TerminalRepr): self.lineno = lineno self.message = message - def toterminal(self, tw: py.io.TerminalWriter) -> None: + def toterminal(self, tw: TerminalWriter) -> None: # filename and lineno output for each entry, # using an output format that most editors understand msg = self.message @@ -1087,7 +1088,7 @@ class ReprLocals(TerminalRepr): def __init__(self, lines: Sequence[str]) -> None: self.lines = lines - def toterminal(self, tw: py.io.TerminalWriter) -> None: + def toterminal(self, tw: TerminalWriter) -> None: for line in self.lines: tw.line(line) @@ -1096,7 +1097,7 @@ class ReprFuncArgs(TerminalRepr): def __init__(self, args: Sequence[Tuple[str, object]]) -> None: self.args = args - def toterminal(self, tw: py.io.TerminalWriter) -> None: + def toterminal(self, tw: TerminalWriter) -> None: if self.args: linesofar = "" for name, value in self.args: diff --git a/src/_pytest/_io/__init__.py b/src/_pytest/_io/__init__.py index e69de29bb..047bb179a 100644 --- a/src/_pytest/_io/__init__.py +++ b/src/_pytest/_io/__init__.py @@ -0,0 +1,3 @@ +# Reexport TerminalWriter from here instead of py, to make it easier to +# extend or swap our own implementation in the future. +from py.io import TerminalWriter as TerminalWriter # noqa: F401 diff --git a/src/_pytest/cacheprovider.py b/src/_pytest/cacheprovider.py index 802c52122..97b51617b 100755 --- a/src/_pytest/cacheprovider.py +++ b/src/_pytest/cacheprovider.py @@ -17,6 +17,7 @@ from .pathlib import Path from .pathlib import resolve_from_str from .pathlib import rm_rf from _pytest import nodes +from _pytest._io import TerminalWriter from _pytest.config import Config from _pytest.main import Session @@ -418,7 +419,7 @@ def pytest_report_header(config): def cacheshow(config, session): from pprint import pformat - tw = py.io.TerminalWriter() + tw = TerminalWriter() tw.line("cachedir: " + str(config.cache._cachedir)) if not config.cache._cachedir.is_dir(): tw.line("cache is empty") diff --git a/src/_pytest/config/__init__.py b/src/_pytest/config/__init__.py index 2677c2bec..3516b333e 100644 --- a/src/_pytest/config/__init__.py +++ b/src/_pytest/config/__init__.py @@ -36,6 +36,7 @@ from .findpaths import determine_setup from .findpaths import exists from _pytest._code import ExceptionInfo from _pytest._code import filter_traceback +from _pytest._io import TerminalWriter from _pytest.compat import importlib_metadata from _pytest.compat import TYPE_CHECKING from _pytest.outcomes import fail @@ -73,7 +74,7 @@ def main(args=None, plugins=None) -> "Union[int, _pytest.main.ExitCode]": config = _prepareconfig(args, plugins) except ConftestImportFailure as e: exc_info = ExceptionInfo(e.excinfo) - tw = py.io.TerminalWriter(sys.stderr) + tw = TerminalWriter(sys.stderr) tw.line( "ImportError while loading conftest '{e.path}'.".format(e=e), red=True ) @@ -99,7 +100,7 @@ def main(args=None, plugins=None) -> "Union[int, _pytest.main.ExitCode]": finally: config._ensure_unconfigure() except UsageError as e: - tw = py.io.TerminalWriter(sys.stderr) + tw = TerminalWriter(sys.stderr) for msg in e.args: tw.line("ERROR: {}\n".format(msg), red=True) return ExitCode.USAGE_ERROR @@ -1175,12 +1176,12 @@ def setns(obj, dic): setattr(pytest, name, value) -def create_terminal_writer(config, *args, **kwargs): +def create_terminal_writer(config: Config, *args, **kwargs) -> TerminalWriter: """Create a TerminalWriter instance configured according to the options in the config object. Every code which requires a TerminalWriter object and has access to a config object should use this function. """ - tw = py.io.TerminalWriter(*args, **kwargs) + tw = TerminalWriter(*args, **kwargs) if config.option.color == "yes": tw.hasmarkup = True if config.option.color == "no": diff --git a/src/_pytest/doctest.py b/src/_pytest/doctest.py index d7ca888cc..c2fd3fd6d 100644 --- a/src/_pytest/doctest.py +++ b/src/_pytest/doctest.py @@ -13,13 +13,14 @@ from typing import Sequence from typing import Tuple from typing import Union -import py +import py.path import pytest from _pytest import outcomes from _pytest._code.code import ExceptionInfo from _pytest._code.code import ReprFileLocation from _pytest._code.code import TerminalRepr +from _pytest._io import TerminalWriter from _pytest.compat import safe_getattr from _pytest.compat import TYPE_CHECKING from _pytest.fixtures import FixtureRequest @@ -139,7 +140,7 @@ class ReprFailDoctest(TerminalRepr): ): self.reprlocation_lines = reprlocation_lines - def toterminal(self, tw: py.io.TerminalWriter) -> None: + def toterminal(self, tw: TerminalWriter) -> None: for reprlocation, lines in self.reprlocation_lines: for line in lines: tw.line(line) diff --git a/src/_pytest/fixtures.py b/src/_pytest/fixtures.py index f0a1a2ed0..464828de4 100644 --- a/src/_pytest/fixtures.py +++ b/src/_pytest/fixtures.py @@ -16,6 +16,7 @@ import py import _pytest from _pytest._code.code import FormattedExcinfo from _pytest._code.code import TerminalRepr +from _pytest._io import TerminalWriter from _pytest.compat import _format_args from _pytest.compat import _PytestWrapper from _pytest.compat import get_real_func @@ -754,7 +755,7 @@ class FixtureLookupErrorRepr(TerminalRepr): self.firstlineno = firstlineno self.argname = argname - def toterminal(self, tw: py.io.TerminalWriter) -> None: + def toterminal(self, tw: TerminalWriter) -> None: # tw.line("FixtureLookupError: %s" %(self.argname), red=True) for tbline in self.tblines: tw.line(tbline.rstrip()) diff --git a/src/_pytest/reports.py b/src/_pytest/reports.py index 79e106a65..3ad67c224 100644 --- a/src/_pytest/reports.py +++ b/src/_pytest/reports.py @@ -18,6 +18,7 @@ from _pytest._code.code import ReprFuncArgs from _pytest._code.code import ReprLocals from _pytest._code.code import ReprTraceback from _pytest._code.code import TerminalRepr +from _pytest._io import TerminalWriter from _pytest.compat import TYPE_CHECKING from _pytest.nodes import Node from _pytest.outcomes import skip @@ -80,7 +81,7 @@ class BaseReport: .. versionadded:: 3.0 """ - tw = py.io.TerminalWriter(stringio=True) + tw = TerminalWriter(stringio=True) tw.hasmarkup = False self.toterminal(tw) exc = tw.stringio.getvalue() diff --git a/testing/code/test_excinfo.py b/testing/code/test_excinfo.py index ae5d30b3a..55e487fe3 100644 --- a/testing/code/test_excinfo.py +++ b/testing/code/test_excinfo.py @@ -12,6 +12,7 @@ import pytest from _pytest._code.code import ExceptionChainRepr from _pytest._code.code import ExceptionInfo from _pytest._code.code import FormattedExcinfo +from _pytest._io import TerminalWriter try: @@ -855,7 +856,7 @@ raise ValueError() from _pytest._code.code import TerminalRepr class MyRepr(TerminalRepr): - def toterminal(self, tw: py.io.TerminalWriter) -> None: + def toterminal(self, tw: TerminalWriter) -> None: tw.line("я") x = str(MyRepr()) @@ -1005,7 +1006,7 @@ raise ValueError() """ ) excinfo = pytest.raises(ValueError, mod.f) - tw = py.io.TerminalWriter(stringio=True) + tw = TerminalWriter(stringio=True) repr = excinfo.getrepr(**reproptions) repr.toterminal(tw) assert tw.stringio.getvalue() @@ -1225,7 +1226,7 @@ raise ValueError() getattr(excinfo.value, attr).__traceback__ = None r = excinfo.getrepr() - tw = py.io.TerminalWriter(stringio=True) + tw = TerminalWriter(stringio=True) tw.hasmarkup = False r.toterminal(tw) diff --git a/testing/logging/test_formatter.py b/testing/logging/test_formatter.py index b363e8b03..85e949d7a 100644 --- a/testing/logging/test_formatter.py +++ b/testing/logging/test_formatter.py @@ -1,7 +1,6 @@ import logging -import py.io - +from _pytest._io import TerminalWriter from _pytest.logging import ColoredLevelFormatter @@ -22,7 +21,7 @@ def test_coloredlogformatter(): class option: pass - tw = py.io.TerminalWriter() + tw = TerminalWriter() tw.hasmarkup = True formatter = ColoredLevelFormatter(tw, logfmt) output = formatter.format(record) @@ -142,7 +141,7 @@ def test_colored_short_level(): class option: pass - tw = py.io.TerminalWriter() + tw = TerminalWriter() tw.hasmarkup = True formatter = ColoredLevelFormatter(tw, logfmt) output = formatter.format(record) From a8d67f5e7b1a3240e3732a13aeae155238ea093c Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Thu, 23 Jan 2020 14:44:30 +0100 Subject: [PATCH 13/23] ci: codecov: add flags for GHA/Travis This would help with debugging missing coverage when removing Travis jobs. --- .github/workflows/main.yml | 2 +- .travis.yml | 2 +- scripts/report-coverage.sh | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 8a5ddbda4..a4a5fdd9d 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -154,7 +154,7 @@ jobs: with: token: ${{ secrets.codecov }} file: ./coverage.xml - flags: ${{ runner.os }} + flags: GHA fail_ci_if_error: false name: ${{ matrix.name }} diff --git a/.travis.yml b/.travis.yml index d813cf07a..1520926b0 100644 --- a/.travis.yml +++ b/.travis.yml @@ -71,7 +71,7 @@ script: tox after_success: - | if [[ "$PYTEST_COVERAGE" = 1 ]]; then - env CODECOV_NAME="$TOXENV-$TRAVIS_OS_NAME" scripts/report-coverage.sh + env CODECOV_NAME="$TOXENV-$TRAVIS_OS_NAME" scripts/report-coverage.sh -F Travis fi notifications: diff --git a/scripts/report-coverage.sh b/scripts/report-coverage.sh index 165426a11..fbcf20ca9 100755 --- a/scripts/report-coverage.sh +++ b/scripts/report-coverage.sh @@ -15,4 +15,4 @@ python -m coverage xml python -m coverage report -m # Set --connect-timeout to work around https://github.com/curl/curl/issues/4461 curl -S -L --connect-timeout 5 --retry 6 -s https://codecov.io/bash -o codecov-upload.sh -bash codecov-upload.sh -Z -X fix -f coverage.xml +bash codecov-upload.sh -Z -X fix -f coverage.xml "$@" From 55ebd9f80392e3175a08a62ee49a482d9a406f5b Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Wed, 22 Jan 2020 22:50:32 +0100 Subject: [PATCH 14/23] doc: minor fixes for the release process --- HOWTORELEASE.rst | 2 +- scripts/release.minor.rst | 6 +++--- scripts/release.patch.rst | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/HOWTORELEASE.rst b/HOWTORELEASE.rst index d0704b172..f15182e4c 100644 --- a/HOWTORELEASE.rst +++ b/HOWTORELEASE.rst @@ -8,7 +8,7 @@ taking a lot of time to make a new one. .. important:: pytest releases must be prepared on **Linux** because the docs and examples expect - to be executed in that platform. + to be executed on that platform. #. Create a branch ``release-X.Y.Z`` with the version for the release. diff --git a/scripts/release.minor.rst b/scripts/release.minor.rst index 9a488edbc..f71f9b1b6 100644 --- a/scripts/release.minor.rst +++ b/scripts/release.minor.rst @@ -6,7 +6,7 @@ The pytest team is proud to announce the {version} release! pytest is a mature Python testing tool with more than a 2000 tests against itself, passing on many different interpreters and platforms. -This release contains a number of bugs fixes and improvements, so users are encouraged +This release contains a number of bug fixes and improvements, so users are encouraged to take a look at the CHANGELOG: https://docs.pytest.org/en/latest/changelog.html @@ -15,7 +15,7 @@ For complete documentation, please visit: https://docs.pytest.org/en/latest/ -As usual, you can upgrade from pypi via: +As usual, you can upgrade from PyPI via: pip install -U pytest @@ -24,4 +24,4 @@ Thanks to all who contributed to this release, among them: {contributors} Happy testing, -The Pytest Development Team +The pytest Development Team diff --git a/scripts/release.patch.rst b/scripts/release.patch.rst index b1ad2dbd7..e73d91768 100644 --- a/scripts/release.patch.rst +++ b/scripts/release.patch.rst @@ -3,7 +3,7 @@ pytest-{version} pytest {version} has just been released to PyPI. -This is a bug-fix release, being a drop-in replacement. To upgrade:: +This is a bug fix release, being a drop-in replacement. To upgrade:: pip install --upgrade pytest From 83451b548fc3c86d17275e7a3b12e4238167aea3 Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Thu, 23 Jan 2020 19:08:32 +0100 Subject: [PATCH 15/23] doc/en/fixture.rst: chdir back to previous directory This is considered to be best practice, and should be used in docs therefore. --- doc/en/fixture.rst | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/doc/en/fixture.rst b/doc/en/fixture.rst index db06a4015..2094027f3 100644 --- a/doc/en/fixture.rst +++ b/doc/en/fixture.rst @@ -1042,11 +1042,13 @@ file: import pytest - @pytest.fixture() + @pytest.fixture def cleandir(): + old_cwd = os.getcwd() newpath = tempfile.mkdtemp() os.chdir(newpath) yield + os.chdir(old_cwd) shutil.rmtree(newpath) and declare its use in a test module via a ``usefixtures`` marker: From cdd6f86e43efd0bd42aed7c51a0afb8ab2518702 Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Thu, 23 Jan 2020 20:34:21 +0100 Subject: [PATCH 16/23] bug-fix fixes --- CONTRIBUTING.rst | 6 +++--- HOWTORELEASE.rst | 2 +- changelog/README.rst | 2 +- doc/en/changelog.rst | 4 ++-- doc/en/development_guide.rst | 2 +- scripts/release.patch.rst | 2 +- 6 files changed, 9 insertions(+), 9 deletions(-) diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst index 455998b78..0474fa3a3 100644 --- a/CONTRIBUTING.rst +++ b/CONTRIBUTING.rst @@ -166,7 +166,7 @@ Short version #. Fork the repository. #. Enable and install `pre-commit `_ to ensure style-guides and code checks are followed. -#. Target ``master`` for bugfixes and doc changes. +#. Target ``master`` for bug fixes and doc changes. #. Target ``features`` for new features or functionality changes. #. Follow **PEP-8** for naming and `black `_ for formatting. #. Tests are run using ``tox``:: @@ -212,7 +212,7 @@ Here is a simple overview, with pytest-specific bits: $ git checkout -b your-feature-branch-name features - Given we have "major.minor.micro" version numbers, bugfixes will usually + Given we have "major.minor.micro" version numbers, bug fixes will usually be released in micro releases whereas features will be released in minor releases and incompatible changes in major releases. @@ -294,7 +294,7 @@ Here is a simple overview, with pytest-specific bits: compare: your-branch-name base-fork: pytest-dev/pytest - base: master # if it's a bugfix + base: master # if it's a bug fix base: features # if it's a feature diff --git a/HOWTORELEASE.rst b/HOWTORELEASE.rst index f15182e4c..b6b596ba2 100644 --- a/HOWTORELEASE.rst +++ b/HOWTORELEASE.rst @@ -1,7 +1,7 @@ Release Procedure ----------------- -Our current policy for releasing is to aim for a bugfix every few weeks and a minor release every 2-3 months. The idea +Our current policy for releasing is to aim for a bug-fix release every few weeks and a minor release every 2-3 months. The idea is to get fixes and new features out instead of trying to cram a ton of features into a release and by consequence taking a lot of time to make a new one. diff --git a/changelog/README.rst b/changelog/README.rst index adabc9ca1..0a819d74e 100644 --- a/changelog/README.rst +++ b/changelog/README.rst @@ -15,7 +15,7 @@ Each file should be named like ``..rst``, where * ``feature``: new user facing features, like new command-line options and new behavior. * ``improvement``: improvement of existing functionality, usually without requiring user intervention (for example, new fields being written in ``--junitxml``, improved colors in terminal, etc). -* ``bugfix``: fixes a reported bug. +* ``bugfix``: fixes a bug. * ``doc``: documentation improvement, like rewording an entire session or adding missing docs. * ``deprecation``: feature deprecation. * ``removal``: feature removal. diff --git a/doc/en/changelog.rst b/doc/en/changelog.rst index 0bda6bb54..e0a2495cc 100644 --- a/doc/en/changelog.rst +++ b/doc/en/changelog.rst @@ -2357,7 +2357,7 @@ Deprecations and Removals - `#4036 `_: The ``item`` parameter of ``pytest_warning_captured`` hook is now documented as deprecated. We realized only after the ``3.8`` release that this parameter is incompatible with ``pytest-xdist``. - Our policy is to not deprecate features during bugfix releases, but in this case we believe it makes sense as we are + Our policy is to not deprecate features during bug-fix releases, but in this case we believe it makes sense as we are only documenting it as deprecated, without issuing warnings which might potentially break test suites. This will get the word out that hook implementers should not use this parameter at all. @@ -5380,7 +5380,7 @@ time or change existing behaviors in order to make them less surprising/more use Thanks Daniel Grunwald for the report and Bruno Oliveira for the PR. - (experimental) adapt more SEMVER style versioning and change meaning of - master branch in git repo: "master" branch now keeps the bugfixes, changes + master branch in git repo: "master" branch now keeps the bug fixes, changes aimed for micro releases. "features" branch will only be released with minor or major pytest releases. diff --git a/doc/en/development_guide.rst b/doc/en/development_guide.rst index 649419316..31fc2c438 100644 --- a/doc/en/development_guide.rst +++ b/doc/en/development_guide.rst @@ -19,7 +19,7 @@ Branches We have two long term branches: -* ``master``: contains the code for the next bugfix release. +* ``master``: contains the code for the next bug-fix release. * ``features``: contains the code with new features for the next minor release. The official repository usually does not contain topic branches, developers and contributors should create topic diff --git a/scripts/release.patch.rst b/scripts/release.patch.rst index e73d91768..b1ad2dbd7 100644 --- a/scripts/release.patch.rst +++ b/scripts/release.patch.rst @@ -3,7 +3,7 @@ pytest-{version} pytest {version} has just been released to PyPI. -This is a bug fix release, being a drop-in replacement. To upgrade:: +This is a bug-fix release, being a drop-in replacement. To upgrade:: pip install --upgrade pytest From c051a9e7b918dffb87879fbd2ee07a6a41c0b1e6 Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Fri, 24 Jan 2020 00:22:24 +0100 Subject: [PATCH 17/23] ci: GHA: separate jobs for linting, docs, doctesting It helps to know upfront that e.g. linting failed, and makes finding the error easier. --- .github/workflows/main.yml | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index a4a5fdd9d..607cb5877 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -43,6 +43,8 @@ jobs: "macos-py38", "linting", + "docs", + "doctesting", ] include: @@ -112,7 +114,17 @@ jobs: - name: "linting" python: "3.7" os: ubuntu-latest - tox_env: "linting,docs,doctesting" + tox_env: "linting" + skip_coverage: true + - name: "docs" + python: "3.7" + os: ubuntu-latest + tox_env: "docs" + skip_coverage: true + - name: "doctesting" + python: "3.7" + os: ubuntu-latest + tox_env: "doctesting" steps: - uses: actions/checkout@v1 From 79ae86cc3f76d69460e1c7beca4ce95e68ab80a6 Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Tue, 21 Jan 2020 20:06:14 +0100 Subject: [PATCH 18/23] tests: fix test_repr_traceback_with_invalid_cwd This never worked as expected (since a912d3745), and only py38-windows triggered the mocked `os.getcwd` unintentionally, via `inspect`. --- testing/code/test_excinfo.py | 40 +++++++++++++++++++++++++++++------- 1 file changed, 33 insertions(+), 7 deletions(-) diff --git a/testing/code/test_excinfo.py b/testing/code/test_excinfo.py index 55e487fe3..c67201191 100644 --- a/testing/code/test_excinfo.py +++ b/testing/code/test_excinfo.py @@ -13,7 +13,7 @@ from _pytest._code.code import ExceptionChainRepr from _pytest._code.code import ExceptionInfo from _pytest._code.code import FormattedExcinfo from _pytest._io import TerminalWriter - +from _pytest.pytester import LineMatcher try: import importlib @@ -776,14 +776,43 @@ raise ValueError() ) excinfo = pytest.raises(ValueError, mod.entry) - p = FormattedExcinfo() + p = FormattedExcinfo(abspath=False) + + raised = 0 + + orig_getcwd = os.getcwd def raiseos(): - raise OSError(2) + nonlocal raised + if sys._getframe().f_back.f_code.co_name == "checked_call": + # Only raise with expected calls, but not via e.g. inspect for + # py38-windows. + raised += 1 + raise OSError(2, "custom_oserror") + return orig_getcwd() monkeypatch.setattr(os, "getcwd", raiseos) assert p._makepath(__file__) == __file__ - p.repr_traceback(excinfo) + assert raised == 1 + repr_tb = p.repr_traceback(excinfo) + + matcher = LineMatcher(str(repr_tb).splitlines()) + matcher.fnmatch_lines( + [ + "def entry():", + "> f(0)", + "", + "{}:5: ".format(mod.__file__), + "_ _ *", + "", + " def f(x):", + "> raise ValueError(x)", + "E ValueError: 0", + "", + "{}:3: ValueError".format(mod.__file__), + ] + ) + assert raised == 3 def test_repr_excinfo_addouterr(self, importasmod, tw_mock): mod = importasmod( @@ -1201,8 +1230,6 @@ raise ValueError() real traceback, such as those raised in a subprocess submitted by the multiprocessing module (#1984). """ - from _pytest.pytester import LineMatcher - exc_handling_code = " from e" if reason == "cause" else "" mod = importasmod( """ @@ -1321,7 +1348,6 @@ def test_exception_repr_extraction_error_on_recursion(): Ensure we can properly detect a recursion error even if some locals raise error on comparison (#2459). """ - from _pytest.pytester import LineMatcher class numpy_like: def __eq__(self, other): From f8654e6656b7bdbc714224d2132fe6b73423f676 Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Fri, 24 Jan 2020 17:26:10 +0100 Subject: [PATCH 19/23] ci: Travis: removing linting,docs,doctesting This is handled by GHA already, and not affected/required for coverage. --- .travis.yml | 5 ----- 1 file changed, 5 deletions(-) diff --git a/.travis.yml b/.travis.yml index 1520926b0..59c7951e4 100644 --- a/.travis.yml +++ b/.travis.yml @@ -47,11 +47,6 @@ jobs: python: '3.5.1' dist: trusty - - env: TOXENV=linting,docs,doctesting PYTEST_COVERAGE=1 - cache: - directories: - - $HOME/.cache/pre-commit - before_script: - | # Do not (re-)upload coverage with cron runs. From e5a362d0f52121572fb13b9fdcdf05df6490541a Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Fri, 24 Jan 2020 11:06:10 +0100 Subject: [PATCH 20/23] ci: GHA: use scripts/report-coverage.sh --- .github/workflows/main.yml | 16 ++-------------- 1 file changed, 2 insertions(+), 14 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 607cb5877..1f16be6e6 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -154,21 +154,9 @@ jobs: run: | python scripts/append_codecov_token.py - - name: Combine coverage + - name: Report coverage if: (!matrix.skip_coverage) - run: | - python -m coverage combine - python -m coverage xml - - - name: Codecov upload - if: (!matrix.skip_coverage) - uses: codecov/codecov-action@v1 - with: - token: ${{ secrets.codecov }} - file: ./coverage.xml - flags: GHA - fail_ci_if_error: false - name: ${{ matrix.name }} + run: bash scripts/report-coverage.sh -F GHA deploy: if: github.event_name == 'push' && startsWith(github.event.ref, 'refs/tags') && github.repository == 'pytest-dev/pytest' From 934f38995a5277b1e62550a3729a7ae18b840f8d Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Fri, 24 Jan 2020 19:40:55 +0100 Subject: [PATCH 21/23] ci: GHA: codecov: set CODECOV_NAME, OS in flags --- .github/workflows/main.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 1f16be6e6..c55316874 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -156,7 +156,9 @@ jobs: - name: Report coverage if: (!matrix.skip_coverage) - run: bash scripts/report-coverage.sh -F GHA + env: + CODECOV_NAME: ${{ matrix.name }} + run: bash scripts/report-coverage.sh -F GHA,${{ runner.os }} deploy: if: github.event_name == 'push' && startsWith(github.event.ref, 'refs/tags') && github.repository == 'pytest-dev/pytest' From e2fa78c99fd84f90e7fb7f1d7a48ef0e705f5b23 Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Fri, 24 Jan 2020 19:49:57 +0100 Subject: [PATCH 22/23] ci: remove Azure config/scripts This is covered by GitHub Actions now. --- azure-pipelines.yml | 80 --------------------------------------------- scripts/retry.cmd | 21 ------------ 2 files changed, 101 deletions(-) delete mode 100644 azure-pipelines.yml delete mode 100644 scripts/retry.cmd diff --git a/azure-pipelines.yml b/azure-pipelines.yml deleted file mode 100644 index a6d856d91..000000000 --- a/azure-pipelines.yml +++ /dev/null @@ -1,80 +0,0 @@ -trigger: -- master -- features - -variables: - PYTEST_ADDOPTS: "--junitxml=build/test-results/$(tox.env).xml -vv" - PYTEST_COVERAGE: '0' - -jobs: - -- job: 'Test' - pool: - vmImage: "vs2017-win2016" - strategy: - matrix: - # -- pypy3 disabled for now: #5279 -- - # pypy3: - # python.version: 'pypy3' - # tox.env: 'pypy3' - py35-xdist: - python.version: '3.5' - tox.env: 'py35-xdist' - # Coverage for: - # - test_supports_breakpoint_module_global - PYTEST_COVERAGE: '1' - py36-xdist: - python.version: '3.6' - tox.env: 'py36-xdist' - py37: - python.version: '3.7' - tox.env: 'py37-twisted-numpy' - # Coverage for: - # - _py36_windowsconsoleio_workaround (with py36+) - # - test_request_garbage (no xdist) - PYTEST_COVERAGE: '1' - py37-linting/docs/doctesting: - python.version: '3.7' - tox.env: 'linting,docs,doctesting' - py37-pluggymaster-xdist: - python.version: '3.7' - tox.env: 'py37-pluggymaster-xdist' - py38-xdist: - python.version: '3.8' - tox.env: 'py38-xdist' - maxParallel: 10 - - steps: - - task: UsePythonVersion@0 - inputs: - versionSpec: '$(python.version)' - architecture: 'x64' - - - script: python -m pip install --upgrade pip && python -m pip install tox - displayName: 'Install tox' - - - bash: | - if [[ "$PYTEST_COVERAGE" == "1" ]]; then - export _PYTEST_TOX_COVERAGE_RUN="coverage run -m" - export _PYTEST_TOX_EXTRA_DEP=coverage-enable-subprocess - export COVERAGE_FILE="$PWD/.coverage" - export COVERAGE_PROCESS_START="$PWD/.coveragerc" - fi - 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() - - - bash: | - if [[ "$PYTEST_COVERAGE" == 1 ]]; then - scripts/report-coverage.sh - fi - env: - CODECOV_NAME: $(tox.env) - CODECOV_TOKEN: $(CODECOV_TOKEN) - displayName: Report and upload coverage - condition: eq(variables['PYTEST_COVERAGE'], '1') diff --git a/scripts/retry.cmd b/scripts/retry.cmd deleted file mode 100644 index ac3836508..000000000 --- a/scripts/retry.cmd +++ /dev/null @@ -1,21 +0,0 @@ -@echo off -rem Source: https://github.com/appveyor/ci/blob/master/scripts/appveyor-retry.cmd -rem initiate the retry number -set retryNumber=0 -set maxRetries=3 - -:RUN -%* -set LastErrorLevel=%ERRORLEVEL% -IF %LastErrorLevel% == 0 GOTO :EOF -set /a retryNumber=%retryNumber%+1 -IF %reTryNumber% == %maxRetries% (GOTO :FAILED) - -:RETRY -set /a retryNumberDisp=%retryNumber%+1 -@echo Command "%*" failed with exit code %LastErrorLevel%. Retrying %retryNumberDisp% of %maxRetries% -GOTO :RUN - -: FAILED -@echo Sorry, we tried running command for %maxRetries% times and all attempts were unsuccessful! -EXIT /B %LastErrorLevel% From 192d3adda34755dc96156274680fca4fd97b4720 Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Fri, 24 Jan 2020 12:44:51 +0100 Subject: [PATCH 23/23] tests: add test_fixture_arg_ordering This is a regression test for part of https://github.com/pytest-dev/pytest/issues/6492, testing one of the fixes in https://github.com/pytest-dev/pytest/pull/6551. --- testing/python/fixtures.py | 35 +++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/testing/python/fixtures.py b/testing/python/fixtures.py index 26374bc34..8cfaae50d 100644 --- a/testing/python/fixtures.py +++ b/testing/python/fixtures.py @@ -4207,3 +4207,38 @@ def test_fixture_parametrization_nparray(testdir): ) result = testdir.runpytest() result.assert_outcomes(passed=10) + + +def test_fixture_arg_ordering(testdir): + """ + This test describes how fixtures in the same scope but without explicit dependencies + between them are created. While users should make dependencies explicit, often + they rely on this order, so this test exists to catch regressions in this regard. + See #6540 and #6492. + """ + p1 = testdir.makepyfile( + """ + import pytest + + suffixes = [] + + @pytest.fixture + def fix_1(): suffixes.append("fix_1") + @pytest.fixture + def fix_2(): suffixes.append("fix_2") + @pytest.fixture + def fix_3(): suffixes.append("fix_3") + @pytest.fixture + def fix_4(): suffixes.append("fix_4") + @pytest.fixture + def fix_5(): suffixes.append("fix_5") + + @pytest.fixture + def fix_combined(fix_1, fix_2, fix_3, fix_4, fix_5): pass + + def test_suffix(fix_combined): + assert suffixes == ["fix_1", "fix_2", "fix_3", "fix_4", "fix_5"] + """ + ) + result = testdir.runpytest("-vv", str(p1)) + assert result.ret == 0