From ab40696007b9f2313e26d0bcf67eb04fbb9d0efc Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Thu, 6 Sep 2018 18:02:36 -0300 Subject: [PATCH 001/105] -W now takes precedence over filters in ini files Fix #3946 --- changelog/3946.bugfix.rst | 2 ++ src/_pytest/warnings.py | 12 ++++++----- testing/test_warnings.py | 44 +++++++++++++++++++++++++++++++++++++++ 3 files changed, 53 insertions(+), 5 deletions(-) create mode 100644 changelog/3946.bugfix.rst diff --git a/changelog/3946.bugfix.rst b/changelog/3946.bugfix.rst new file mode 100644 index 000000000..1b5248b4c --- /dev/null +++ b/changelog/3946.bugfix.rst @@ -0,0 +1,2 @@ +Warning filters passed as command line options using ``-W`` now take precedence over filters defined in ``ini`` +configuration files. diff --git a/src/_pytest/warnings.py b/src/_pytest/warnings.py index 6c4b921fa..fb303a20f 100644 --- a/src/_pytest/warnings.py +++ b/src/_pytest/warnings.py @@ -67,17 +67,19 @@ def catch_warnings_for_item(config, ihook, when, item): Each warning captured triggers the ``pytest_warning_captured`` hook. """ - args = config.getoption("pythonwarnings") or [] + cmdline_filters = config.getoption("pythonwarnings") or [] inifilters = config.getini("filterwarnings") with warnings.catch_warnings(record=True) as log: - filters_configured = args or inifilters or sys.warnoptions - - for arg in args: - warnings._setoption(arg) + filters_configured = bool(cmdline_filters or inifilters or sys.warnoptions) + # filters should have this precedence: mark, cmdline options, ini + # filters should be applied in the inverse order of precedence for arg in inifilters: _setoption(warnings, arg) + for arg in cmdline_filters: + warnings._setoption(arg) + if item is not None: for mark in item.iter_markers(name="filterwarnings"): for arg in mark.args: diff --git a/testing/test_warnings.py b/testing/test_warnings.py index 3f748d666..975605b5e 100644 --- a/testing/test_warnings.py +++ b/testing/test_warnings.py @@ -430,6 +430,50 @@ def test_hide_pytest_internal_warnings(testdir, ignore_pytest_warnings): ) +@pytest.mark.parametrize("ignore_on_cmdline", [True, False]) +def test_option_precedence_cmdline_over_ini(testdir, ignore_on_cmdline): + """filters defined in the command-line should take precedence over filters in ini files (#3946).""" + testdir.makeini( + """ + [pytest] + filterwarnings = error + """ + ) + testdir.makepyfile( + """ + import warnings + def test(): + warnings.warn(UserWarning('hello')) + """ + ) + args = ["-W", "ignore"] if ignore_on_cmdline else [] + result = testdir.runpytest(*args) + if ignore_on_cmdline: + result.stdout.fnmatch_lines(["* 1 passed in*"]) + else: + result.stdout.fnmatch_lines(["* 1 failed in*"]) + + +def test_option_precedence_mark(testdir): + """Filters defined by marks should always take precedence (#3946).""" + testdir.makeini( + """ + [pytest] + filterwarnings = ignore + """ + ) + testdir.makepyfile( + """ + import pytest, warnings + @pytest.mark.filterwarnings('error') + def test(): + warnings.warn(UserWarning('hello')) + """ + ) + result = testdir.runpytest("-W", "ignore") + result.stdout.fnmatch_lines(["* 1 failed in*"]) + + class TestDeprecationWarningsByDefault: """ Note: all pytest runs are executed in a subprocess so we don't inherit warning filters From 495a55725b5f85ca6935ff2d67a2fd62f915e924 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Thu, 13 Sep 2018 14:02:01 -0300 Subject: [PATCH 002/105] Separate deprecations and removals in the CHANGELOG --- changelog/README.rst | 3 ++- pyproject.toml | 7 ++++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/changelog/README.rst b/changelog/README.rst index 47e21fb33..e471409b0 100644 --- a/changelog/README.rst +++ b/changelog/README.rst @@ -14,7 +14,8 @@ Each file should be named like ``..rst``, where * ``feature``: new user facing features, like new command-line options and new behavior. * ``bugfix``: fixes a reported bug. * ``doc``: documentation improvement, like rewording an entire session or adding missing docs. -* ``removal``: feature deprecation or removal. +* ``deprecation``: feature deprecation. +* ``removal``: feature removal. * ``vendor``: changes in packages vendored in pytest. * ``trivial``: fixing a small typo or internal change that might be noteworthy. diff --git a/pyproject.toml b/pyproject.toml index b1e85601e..e82f051e1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -15,7 +15,12 @@ template = "changelog/_template.rst" [[tool.towncrier.type]] directory = "removal" - name = "Deprecations and Removals" + name = "Removals" + showcontent = true + + [[tool.towncrier.type]] + directory = "deprecation" + name = "Deprecations" showcontent = true [[tool.towncrier.type]] From bf074b37a3192a454f8c156e116eebd0b4588d9e Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Thu, 13 Sep 2018 14:04:29 -0300 Subject: [PATCH 003/105] Show deprecation warnings for compat properties Fix #3616 --- changelog/3616.deprecation.rst | 8 ++++++++ doc/en/historical-notes.rst | 10 ++++++++++ src/_pytest/nodes.py | 13 ++++++++----- src/_pytest/python.py | 2 +- testing/deprecated_test.py | 17 +++++++++++++++++ 5 files changed, 44 insertions(+), 6 deletions(-) create mode 100644 changelog/3616.deprecation.rst diff --git a/changelog/3616.deprecation.rst b/changelog/3616.deprecation.rst new file mode 100644 index 000000000..e02e83c20 --- /dev/null +++ b/changelog/3616.deprecation.rst @@ -0,0 +1,8 @@ +The following accesses have been documented as deprecated for years, but are now actually emitting deprecation warnings. + +* Access of ``Module``, ``Function``, ``Class``, ``Instance``, ``File`` and ``Item`` through ``Node`` instances. Now + users will this warning:: + + usage of Function.Module is deprecated, please use pytest.Module instead + + Users should just ``import pytest`` and access those objects using the ``pytest`` module. diff --git a/doc/en/historical-notes.rst b/doc/en/historical-notes.rst index 028ceff9b..9462d700f 100644 --- a/doc/en/historical-notes.rst +++ b/doc/en/historical-notes.rst @@ -175,3 +175,13 @@ Previous to version 2.4 to set a break point in code one needed to use ``pytest. This is no longer needed and one can use the native ``import pdb;pdb.set_trace()`` call directly. For more details see :ref:`breakpoints`. + +"compat" properties +------------------- + +.. deprecated:: 3.9 + +Access of ``Module``, ``Function``, ``Class``, ``Instance``, ``File`` and ``Item`` through ``Node`` instances have long +been documented as deprecated, but started to emit warnings from pytest ``3.9`` and onward. + +Users should just ``import pytest`` and access those objects using the ``pytest`` module. diff --git a/src/_pytest/nodes.py b/src/_pytest/nodes.py index 29d1f0a87..222059d8c 100644 --- a/src/_pytest/nodes.py +++ b/src/_pytest/nodes.py @@ -11,6 +11,7 @@ import _pytest._code from _pytest.compat import getfslineno from _pytest.mark.structures import NodeKeywords, MarkInfo +from _pytest.warning_types import RemovedInPytest4Warning SEP = "/" @@ -61,11 +62,13 @@ class _CompatProperty(object): if obj is None: return self - # TODO: reenable in the features branch - # warnings.warn( - # "usage of {owner!r}.{name} is deprecated, please use pytest.{name} instead".format( - # name=self.name, owner=type(owner).__name__), - # PendingDeprecationWarning, stacklevel=2) + warnings.warn( + "usage of {owner}.{name} is deprecated, please use pytest.{name} instead".format( + name=self.name, owner=owner.__name__ + ), + RemovedInPytest4Warning, + stacklevel=2, + ) return getattr(__import__("pytest"), self.name) diff --git a/src/_pytest/python.py b/src/_pytest/python.py index 051650272..dff7a6950 100644 --- a/src/_pytest/python.py +++ b/src/_pytest/python.py @@ -800,7 +800,7 @@ class Generator(FunctionMixin, PyCollector): "%r generated tests with non-unique name %r" % (self, name) ) seen[name] = True - values.append(self.Function(name, self, args=args, callobj=call)) + values.append(Function(name, self, args=args, callobj=call)) self.warn(deprecated.YIELD_TESTS) return values diff --git a/testing/deprecated_test.py b/testing/deprecated_test.py index fbaca4e30..d53f86e15 100644 --- a/testing/deprecated_test.py +++ b/testing/deprecated_test.py @@ -30,6 +30,23 @@ def test_yield_tests_deprecation(testdir): assert result.stdout.str().count("yield tests are deprecated") == 2 +def test_compat_properties_deprecation(testdir): + testdir.makepyfile( + """ + def test_foo(request): + print(request.node.Module) + """ + ) + result = testdir.runpytest() + result.stdout.fnmatch_lines( + [ + "*test_compat_properties_deprecation.py:2:*usage of Function.Module is deprecated, " + "please use pytest.Module instead*", + "*1 passed, 1 warnings in*", + ] + ) + + @pytest.mark.filterwarnings("default") def test_funcarg_prefix_deprecation(testdir): testdir.makepyfile( From 482bd5efd2a88a025a21979a6d7e7923a0605209 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Thu, 13 Sep 2018 14:25:46 -0300 Subject: [PATCH 004/105] Show deprecation warning for cached_setup --- changelog/3616.deprecation.rst | 3 +++ src/_pytest/fixtures.py | 5 +++++ testing/deprecated_test.py | 21 +++++++++++++++++++++ testing/python/fixture.py | 2 ++ 4 files changed, 31 insertions(+) diff --git a/changelog/3616.deprecation.rst b/changelog/3616.deprecation.rst index e02e83c20..e91d1ad07 100644 --- a/changelog/3616.deprecation.rst +++ b/changelog/3616.deprecation.rst @@ -6,3 +6,6 @@ The following accesses have been documented as deprecated for years, but are now usage of Function.Module is deprecated, please use pytest.Module instead Users should just ``import pytest`` and access those objects using the ``pytest`` module. + +* ``request.cached_setup``, this was the precursor of the setup/teardown mechanism available to fixtures. You can + consult `funcarg comparision section in the docs `_. diff --git a/src/_pytest/fixtures.py b/src/_pytest/fixtures.py index 068e6814c..f2c8085ed 100644 --- a/src/_pytest/fixtures.py +++ b/src/_pytest/fixtures.py @@ -479,6 +479,11 @@ class FixtureRequest(FuncargnamesCompatAttr): or ``session`` indicating the caching lifecycle of the resource. :arg extrakey: added to internal caching key of (funcargname, scope). """ + msg = ( + "cached_setup is deprecated and will be removed in a future release. " + "Use standard fixture functions instead." + ) + warnings.warn(RemovedInPytest4Warning(msg), stacklevel=2) if not hasattr(self.config, "_setupcache"): self.config._setupcache = {} # XXX weakref? cachekey = (self.fixturename, self._getscopeitem(scope), extrakey) diff --git a/testing/deprecated_test.py b/testing/deprecated_test.py index d53f86e15..7ca8e6bae 100644 --- a/testing/deprecated_test.py +++ b/testing/deprecated_test.py @@ -47,6 +47,27 @@ def test_compat_properties_deprecation(testdir): ) +def test_cached_setup_deprecation(testdir): + testdir.makepyfile( + """ + import pytest + @pytest.fixture + def fix(request): + return request.cached_setup(lambda: 1) + + def test_foo(fix): + assert fix == 1 + """ + ) + result = testdir.runpytest() + result.stdout.fnmatch_lines( + [ + "*test_cached_setup_deprecation.py:4:*cached_setup is deprecated*", + "*1 passed, 1 warnings in*", + ] + ) + + @pytest.mark.filterwarnings("default") def test_funcarg_prefix_deprecation(testdir): testdir.makepyfile( diff --git a/testing/python/fixture.py b/testing/python/fixture.py index fc3eee42b..4e44bf961 100644 --- a/testing/python/fixture.py +++ b/testing/python/fixture.py @@ -977,6 +977,7 @@ class TestRequestCachedSetup(object): ) reprec.assertoutcome(passed=4) + @pytest.mark.filterwarnings("ignore:cached_setup is deprecated") def test_request_cachedsetup_extrakey(self, testdir): item1 = testdir.getitem("def test_func(): pass") req1 = fixtures.FixtureRequest(item1) @@ -994,6 +995,7 @@ class TestRequestCachedSetup(object): assert ret1 == ret1b assert ret2 == ret2b + @pytest.mark.filterwarnings("ignore:cached_setup is deprecated") def test_request_cachedsetup_cache_deletion(self, testdir): item1 = testdir.getitem("def test_func(): pass") req1 = fixtures.FixtureRequest(item1) From b7dd9154c370b5d22ca921cdac4962ca3ca8aacd Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Thu, 13 Sep 2018 14:55:28 -0300 Subject: [PATCH 005/105] Deprecate custom node types during collection by using special names --- changelog/3616.deprecation.rst | 7 +++++++ src/_pytest/nodes.py | 11 +++++++---- testing/deprecated_test.py | 30 ++++++++++++++++++++++++++++++ 3 files changed, 44 insertions(+), 4 deletions(-) diff --git a/changelog/3616.deprecation.rst b/changelog/3616.deprecation.rst index e91d1ad07..04d4bb509 100644 --- a/changelog/3616.deprecation.rst +++ b/changelog/3616.deprecation.rst @@ -9,3 +9,10 @@ The following accesses have been documented as deprecated for years, but are now * ``request.cached_setup``, this was the precursor of the setup/teardown mechanism available to fixtures. You can consult `funcarg comparision section in the docs `_. + +* Using objects named ``"Class"`` as a way to customize the type of nodes that are collected in ``Collector`` + subclasses has been deprecated. Users instead should use ``pytest_collect_make_item`` to customize node types during + collection. + + This issue should affect only advanced plugins who create new collection types, so if you see this warning + message please contact the authors so they can change the code. diff --git a/src/_pytest/nodes.py b/src/_pytest/nodes.py index 222059d8c..8fc11eb4c 100644 --- a/src/_pytest/nodes.py +++ b/src/_pytest/nodes.py @@ -130,10 +130,13 @@ class Node(object): return getattr(__import__("pytest"), name) else: cls = getattr(self, name) - # TODO: reenable in the features branch - # warnings.warn("use of node.%s is deprecated, " - # "use pytest_pycollect_makeitem(...) to create custom " - # "collection nodes" % name, category=DeprecationWarning) + msg = ( + 'use of special named "%s" objects in collectors of type "%s" to ' + "customize the created nodes is deprecated. " + "Use pytest_pycollect_makeitem(...) to create custom " + "collection nodes instead." % (name, type(self).__name__) + ) + self.warn(RemovedInPytest4Warning(msg)) return cls def __repr__(self): diff --git a/testing/deprecated_test.py b/testing/deprecated_test.py index 7ca8e6bae..49b21ccb1 100644 --- a/testing/deprecated_test.py +++ b/testing/deprecated_test.py @@ -68,6 +68,36 @@ def test_cached_setup_deprecation(testdir): ) +def test_custom_class_deprecation(testdir): + testdir.makeconftest( + """ + import pytest + + class MyModule(pytest.Module): + + class Class(pytest.Class): + pass + + def pytest_pycollect_makemodule(path, parent): + return MyModule(path, parent) + """ + ) + testdir.makepyfile( + """ + class Test: + def test_foo(self): + pass + """ + ) + result = testdir.runpytest() + result.stdout.fnmatch_lines( + [ + '*test_custom_class_deprecation.py:1:*"Class" objects in collectors of type "MyModule*', + "*1 passed, 1 warnings in*", + ] + ) + + @pytest.mark.filterwarnings("default") def test_funcarg_prefix_deprecation(testdir): testdir.makepyfile( From feb8240410194c050c881678c97c0d7246ec1ccb Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Thu, 13 Sep 2018 15:44:02 -0300 Subject: [PATCH 006/105] Use self.Function again during collection --- src/_pytest/python.py | 5 ++++- testing/python/collect.py | 6 ++++++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/src/_pytest/python.py b/src/_pytest/python.py index dff7a6950..a0a0655fd 100644 --- a/src/_pytest/python.py +++ b/src/_pytest/python.py @@ -800,7 +800,10 @@ class Generator(FunctionMixin, PyCollector): "%r generated tests with non-unique name %r" % (self, name) ) seen[name] = True - values.append(Function(name, self, args=args, callobj=call)) + with warnings.catch_warnings(): + # ignore our own deprecation warning + function_class = self.Function + values.append(function_class(name, self, args=args, callobj=call)) self.warn(deprecated.YIELD_TESTS) return values diff --git a/testing/python/collect.py b/testing/python/collect.py index c92de12a0..25d8a8e77 100644 --- a/testing/python/collect.py +++ b/testing/python/collect.py @@ -240,6 +240,9 @@ class TestClass(object): assert result.ret == EXIT_NOTESTSCOLLECTED +@pytest.mark.filterwarnings( + "ignore:usage of Generator.Function is deprecated, please use pytest.Function instead" +) class TestGenerator(object): def test_generative_functions(self, testdir): modcol = testdir.getmodulecol( @@ -1255,6 +1258,9 @@ class TestReportInfo(object): assert lineno == 1 assert msg == "TestClass" + @pytest.mark.filterwarnings( + "ignore:usage of Generator.Function is deprecated, please use pytest.Function instead" + ) def test_generator_reportinfo(self, testdir): modcol = testdir.getmodulecol( """ From 32ee0b9c885d8bdda9edeabd86277ba1cb31dce4 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Thu, 13 Sep 2018 15:56:50 -0300 Subject: [PATCH 007/105] Move warning messages to _pytest.deprecated --- src/_pytest/deprecated.py | 16 ++++++++++++++++ src/_pytest/fixtures.py | 8 +++----- src/_pytest/nodes.py | 19 ++++++++++--------- 3 files changed, 29 insertions(+), 14 deletions(-) diff --git a/src/_pytest/deprecated.py b/src/_pytest/deprecated.py index dea8bbde8..5c98fd43c 100644 --- a/src/_pytest/deprecated.py +++ b/src/_pytest/deprecated.py @@ -18,6 +18,22 @@ YIELD_TESTS = RemovedInPytest4Warning( "yield tests are deprecated, and scheduled to be removed in pytest 4.0" ) +CACHED_SETUP = RemovedInPytest4Warning( + "cached_setup is deprecated and will be removed in a future release. " + "Use standard fixture functions instead." +) + +COMPAT_PROPERTY = ( + "usage of {owner}.{name} is deprecated, please use pytest.{name} instead" +) + +CUSTOM_CLASS = ( + 'use of special named "{name}" objects in collectors of type "{type_name}" to ' + "customize the created nodes is deprecated. " + "Use pytest_pycollect_makeitem(...) to create custom " + "collection nodes instead." +) + FUNCARG_PREFIX = ( '{name}: declaring fixtures using "pytest_funcarg__" prefix is deprecated ' "and scheduled to be removed in pytest 4.0. " diff --git a/src/_pytest/fixtures.py b/src/_pytest/fixtures.py index f2c8085ed..610907342 100644 --- a/src/_pytest/fixtures.py +++ b/src/_pytest/fixtures.py @@ -479,11 +479,9 @@ class FixtureRequest(FuncargnamesCompatAttr): or ``session`` indicating the caching lifecycle of the resource. :arg extrakey: added to internal caching key of (funcargname, scope). """ - msg = ( - "cached_setup is deprecated and will be removed in a future release. " - "Use standard fixture functions instead." - ) - warnings.warn(RemovedInPytest4Warning(msg), stacklevel=2) + from _pytest.deprecated import CACHED_SETUP + + warnings.warn(CACHED_SETUP, stacklevel=2) if not hasattr(self.config, "_setupcache"): self.config._setupcache = {} # XXX weakref? cachekey = (self.fixturename, self._getscopeitem(scope), extrakey) diff --git a/src/_pytest/nodes.py b/src/_pytest/nodes.py index 8fc11eb4c..287bd4181 100644 --- a/src/_pytest/nodes.py +++ b/src/_pytest/nodes.py @@ -62,11 +62,12 @@ class _CompatProperty(object): if obj is None: return self + from _pytest.deprecated import COMPAT_PROPERTY + warnings.warn( - "usage of {owner}.{name} is deprecated, please use pytest.{name} instead".format( - name=self.name, owner=owner.__name__ + RemovedInPytest4Warning( + COMPAT_PROPERTY.format(name=self.name, owner=owner.__name__) ), - RemovedInPytest4Warning, stacklevel=2, ) return getattr(__import__("pytest"), self.name) @@ -129,14 +130,14 @@ class Node(object): if isinstance(maybe_compatprop, _CompatProperty): return getattr(__import__("pytest"), name) else: + from _pytest.deprecated import CUSTOM_CLASS + cls = getattr(self, name) - msg = ( - 'use of special named "%s" objects in collectors of type "%s" to ' - "customize the created nodes is deprecated. " - "Use pytest_pycollect_makeitem(...) to create custom " - "collection nodes instead." % (name, type(self).__name__) + self.warn( + RemovedInPytest4Warning( + CUSTOM_CLASS.format(name=name, type_name=type(self).__name__) + ) ) - self.warn(RemovedInPytest4Warning(msg)) return cls def __repr__(self): From da6830f19b13dba32956f27d0493c26e0ab29322 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Fri, 14 Sep 2018 14:49:05 -0300 Subject: [PATCH 008/105] Introduce UnformattedWarning to keep warning types and messages in _pytest.deprecated --- changelog/3616.deprecation.rst | 4 +++ src/_pytest/config/findpaths.py | 9 ++---- src/_pytest/deprecated.py | 52 +++++++++++++++++++++++++-------- src/_pytest/fixtures.py | 13 ++++----- src/_pytest/nodes.py | 12 ++------ src/_pytest/resultlog.py | 3 +- 6 files changed, 56 insertions(+), 37 deletions(-) diff --git a/changelog/3616.deprecation.rst b/changelog/3616.deprecation.rst index 04d4bb509..8ea1b4d3d 100644 --- a/changelog/3616.deprecation.rst +++ b/changelog/3616.deprecation.rst @@ -16,3 +16,7 @@ The following accesses have been documented as deprecated for years, but are now This issue should affect only advanced plugins who create new collection types, so if you see this warning message please contact the authors so they can change the code. + +* The warning that produces the message below has changed to ``RemovedInPytest4Warning``:: + + getfuncargvalue is deprecated, use getfixturevalue diff --git a/src/_pytest/config/findpaths.py b/src/_pytest/config/findpaths.py index 7480603be..f99430198 100644 --- a/src/_pytest/config/findpaths.py +++ b/src/_pytest/config/findpaths.py @@ -103,21 +103,18 @@ def determine_setup(inifile, args, rootdir_cmd_arg=None, config=None): if inifile: iniconfig = py.iniconfig.IniConfig(inifile) is_cfg_file = str(inifile).endswith(".cfg") - # TODO: [pytest] section in *.cfg files is depricated. Need refactoring. sections = ["tool:pytest", "pytest"] if is_cfg_file else ["pytest"] for section in sections: try: inicfg = iniconfig[section] if is_cfg_file and section == "pytest" and config is not None: from _pytest.deprecated import CFG_PYTEST_SECTION - from _pytest.warning_types import RemovedInPytest4Warning from _pytest.warnings import _issue_config_warning + # TODO: [pytest] section in *.cfg files is deprecated. Need refactoring once + # the deprecation expires. _issue_config_warning( - RemovedInPytest4Warning( - CFG_PYTEST_SECTION.format(filename=str(inifile)) - ), - config, + CFG_PYTEST_SECTION.format(filename=str(inifile)), config ) break except KeyError: diff --git a/src/_pytest/deprecated.py b/src/_pytest/deprecated.py index 5c98fd43c..d5ec5dbbf 100644 --- a/src/_pytest/deprecated.py +++ b/src/_pytest/deprecated.py @@ -4,11 +4,32 @@ that is planned to be removed in the next pytest release. Keeping it in a central location makes it easy to track what is deprecated and should be removed when the time comes. + +All constants defined in this module should be either PytestWarning instances or UnformattedWarning +in case of warnings which need to format their messages. """ from __future__ import absolute_import, division, print_function +import attr + from _pytest.warning_types import RemovedInPytest4Warning + +@attr.s +class UnformattedWarning(object): + """Used to hold warnings that need to format their message at runtime, as opposed to a direct message. + + Using this class avoids to keep all the warning types and messages in this module, avoiding misuse. + """ + + category = attr.ib() + template = attr.ib() + + def format(self, **kwargs): + """Returns an instance of the warning category, formatted with given kwargs""" + return self.category(self.template.format(**kwargs)) + + MAIN_STR_ARGS = RemovedInPytest4Warning( "passing a string to pytest.main() is deprecated, " "pass a list of arguments instead." @@ -23,36 +44,43 @@ CACHED_SETUP = RemovedInPytest4Warning( "Use standard fixture functions instead." ) -COMPAT_PROPERTY = ( - "usage of {owner}.{name} is deprecated, please use pytest.{name} instead" +COMPAT_PROPERTY = UnformattedWarning( + RemovedInPytest4Warning, + "usage of {owner}.{name} is deprecated, please use pytest.{name} instead", ) -CUSTOM_CLASS = ( +CUSTOM_CLASS = UnformattedWarning( + RemovedInPytest4Warning, 'use of special named "{name}" objects in collectors of type "{type_name}" to ' "customize the created nodes is deprecated. " "Use pytest_pycollect_makeitem(...) to create custom " - "collection nodes instead." + "collection nodes instead.", ) -FUNCARG_PREFIX = ( +FUNCARG_PREFIX = UnformattedWarning( + RemovedInPytest4Warning, '{name}: declaring fixtures using "pytest_funcarg__" prefix is deprecated ' "and scheduled to be removed in pytest 4.0. " - "Please remove the prefix and use the @pytest.fixture decorator instead." + "Please remove the prefix and use the @pytest.fixture decorator instead.", ) -FIXTURE_FUNCTION_CALL = ( +FIXTURE_FUNCTION_CALL = UnformattedWarning( + RemovedInPytest4Warning, 'Fixture "{name}" called directly. Fixtures are not meant to be called directly, ' "are created automatically when test functions request them as parameters. " - "See https://docs.pytest.org/en/latest/fixture.html for more information." + "See https://docs.pytest.org/en/latest/fixture.html for more information.", ) -CFG_PYTEST_SECTION = ( - "[pytest] section in {filename} files is deprecated, use [tool:pytest] instead." +CFG_PYTEST_SECTION = UnformattedWarning( + RemovedInPytest4Warning, + "[pytest] section in {filename} files is deprecated, use [tool:pytest] instead.", ) -GETFUNCARGVALUE = "getfuncargvalue is deprecated, use getfixturevalue" +GETFUNCARGVALUE = RemovedInPytest4Warning( + "getfuncargvalue is deprecated, use getfixturevalue" +) -RESULT_LOG = ( +RESULT_LOG = RemovedInPytest4Warning( "--result-log is deprecated and scheduled for removal in pytest 4.0.\n" "See https://docs.pytest.org/en/latest/usage.html#creating-resultlog-format-files for more information." ) diff --git a/src/_pytest/fixtures.py b/src/_pytest/fixtures.py index 610907342..964b16e29 100644 --- a/src/_pytest/fixtures.py +++ b/src/_pytest/fixtures.py @@ -32,7 +32,7 @@ from _pytest.compat import ( get_real_method, _PytestWrapper, ) -from _pytest.deprecated import FIXTURE_FUNCTION_CALL, RemovedInPytest4Warning +from _pytest.deprecated import FIXTURE_FUNCTION_CALL from _pytest.outcomes import fail, TEST_OUTCOME FIXTURE_MSG = 'fixtures cannot have "pytest_funcarg__" prefix and be decorated with @pytest.fixture:\n{}' @@ -515,7 +515,7 @@ class FixtureRequest(FuncargnamesCompatAttr): """ Deprecated, use getfixturevalue. """ from _pytest import deprecated - warnings.warn(deprecated.GETFUNCARGVALUE, DeprecationWarning, stacklevel=2) + warnings.warn(deprecated.GETFUNCARGVALUE, stacklevel=2) return self.getfixturevalue(argname) def _get_active_fixturedef(self, argname): @@ -959,8 +959,9 @@ def wrap_function_to_warning_if_called_directly(function, fixture_marker): used as an argument in a test function. """ is_yield_function = is_generator(function) - msg = FIXTURE_FUNCTION_CALL.format(name=fixture_marker.name or function.__name__) - warning = RemovedInPytest4Warning(msg) + warning = FIXTURE_FUNCTION_CALL.format( + name=fixture_marker.name or function.__name__ + ) if is_yield_function: @@ -1287,9 +1288,7 @@ class FixtureManager(object): filename, lineno = getfslineno(obj) warnings.warn_explicit( - RemovedInPytest4Warning( - deprecated.FUNCARG_PREFIX.format(name=name) - ), + deprecated.FUNCARG_PREFIX.format(name=name), category=None, filename=str(filename), lineno=lineno + 1, diff --git a/src/_pytest/nodes.py b/src/_pytest/nodes.py index 287bd4181..9cd758941 100644 --- a/src/_pytest/nodes.py +++ b/src/_pytest/nodes.py @@ -11,7 +11,6 @@ import _pytest._code from _pytest.compat import getfslineno from _pytest.mark.structures import NodeKeywords, MarkInfo -from _pytest.warning_types import RemovedInPytest4Warning SEP = "/" @@ -65,10 +64,7 @@ class _CompatProperty(object): from _pytest.deprecated import COMPAT_PROPERTY warnings.warn( - RemovedInPytest4Warning( - COMPAT_PROPERTY.format(name=self.name, owner=owner.__name__) - ), - stacklevel=2, + COMPAT_PROPERTY.format(name=self.name, owner=owner.__name__), stacklevel=2 ) return getattr(__import__("pytest"), self.name) @@ -133,11 +129,7 @@ class Node(object): from _pytest.deprecated import CUSTOM_CLASS cls = getattr(self, name) - self.warn( - RemovedInPytest4Warning( - CUSTOM_CLASS.format(name=name, type_name=type(self).__name__) - ) - ) + self.warn(CUSTOM_CLASS.format(name=name, type_name=type(self).__name__)) return cls def __repr__(self): diff --git a/src/_pytest/resultlog.py b/src/_pytest/resultlog.py index 8a972eed7..9ae90e770 100644 --- a/src/_pytest/resultlog.py +++ b/src/_pytest/resultlog.py @@ -31,10 +31,9 @@ def pytest_configure(config): config.pluginmanager.register(config._resultlog) from _pytest.deprecated import RESULT_LOG - from _pytest.warning_types import RemovedInPytest4Warning from _pytest.warnings import _issue_config_warning - _issue_config_warning(RemovedInPytest4Warning(RESULT_LOG), config) + _issue_config_warning(RESULT_LOG, config) def pytest_unconfigure(config): From ae8f3695b52c69df9eb7911e209c82617f816a8f Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Fri, 14 Sep 2018 15:31:20 -0300 Subject: [PATCH 009/105] Move UnformattedWarning to _pytest.warning_types --- src/_pytest/deprecated.py | 18 +----------------- src/_pytest/warning_types.py | 18 ++++++++++++++++++ 2 files changed, 19 insertions(+), 17 deletions(-) diff --git a/src/_pytest/deprecated.py b/src/_pytest/deprecated.py index d5ec5dbbf..69beeab5f 100644 --- a/src/_pytest/deprecated.py +++ b/src/_pytest/deprecated.py @@ -10,24 +10,8 @@ in case of warnings which need to format their messages. """ from __future__ import absolute_import, division, print_function -import attr -from _pytest.warning_types import RemovedInPytest4Warning - - -@attr.s -class UnformattedWarning(object): - """Used to hold warnings that need to format their message at runtime, as opposed to a direct message. - - Using this class avoids to keep all the warning types and messages in this module, avoiding misuse. - """ - - category = attr.ib() - template = attr.ib() - - def format(self, **kwargs): - """Returns an instance of the warning category, formatted with given kwargs""" - return self.category(self.template.format(**kwargs)) +from _pytest.warning_types import UnformattedWarning, RemovedInPytest4Warning MAIN_STR_ARGS = RemovedInPytest4Warning( diff --git a/src/_pytest/warning_types.py b/src/_pytest/warning_types.py index 8861f6f2b..55e1f037a 100644 --- a/src/_pytest/warning_types.py +++ b/src/_pytest/warning_types.py @@ -1,3 +1,6 @@ +import attr + + class PytestWarning(UserWarning): """ Bases: :class:`UserWarning`. @@ -39,4 +42,19 @@ class PytestExperimentalApiWarning(PytestWarning, FutureWarning): ) +@attr.s +class UnformattedWarning(object): + """Used to hold warnings that need to format their message at runtime, as opposed to a direct message. + + Using this class avoids to keep all the warning types and messages in this module, avoiding misuse. + """ + + category = attr.ib() + template = attr.ib() + + def format(self, **kwargs): + """Returns an instance of the warning category, formatted with given kwargs""" + return self.category(self.template.format(**kwargs)) + + PYTESTER_COPY_EXAMPLE = PytestExperimentalApiWarning.simple("testdir.copy_example") From 05155e4db0460cc58befbfd4af8192ef1ea14a88 Mon Sep 17 00:00:00 2001 From: Andrea Cimatoribus Date: Wed, 12 Sep 2018 12:04:45 +0200 Subject: [PATCH 010/105] Fail at parametrize option for empty parameter set Optionally raise an exception when parametrize collects no arguments. Provide the name of the test causing the failure in the exception message. See: #3849 --- src/_pytest/mark/__init__.py | 4 ++-- src/_pytest/mark/structures.py | 8 +++++++ testing/test_mark.py | 39 ++++++++++++++++++++++++++++++++-- 3 files changed, 47 insertions(+), 4 deletions(-) diff --git a/src/_pytest/mark/__init__.py b/src/_pytest/mark/__init__.py index e3918ca6a..eb25cd4aa 100644 --- a/src/_pytest/mark/__init__.py +++ b/src/_pytest/mark/__init__.py @@ -163,9 +163,9 @@ def pytest_configure(config): empty_parameterset = config.getini(EMPTY_PARAMETERSET_OPTION) - if empty_parameterset not in ("skip", "xfail", None, ""): + if empty_parameterset not in ("skip", "xfail", "fail_at_collect", None, ""): raise UsageError( - "{!s} must be one of skip and xfail," + "{!s} must be one of skip, xfail or fail_at_collect" " but it is {!r}".format(EMPTY_PARAMETERSET_OPTION, empty_parameterset) ) diff --git a/src/_pytest/mark/structures.py b/src/_pytest/mark/structures.py index 8e8937d59..2c4635bf4 100644 --- a/src/_pytest/mark/structures.py +++ b/src/_pytest/mark/structures.py @@ -32,11 +32,19 @@ def istestfunc(func): def get_empty_parameterset_mark(config, argnames, func): + from ..nodes import Collector + requested_mark = config.getini(EMPTY_PARAMETERSET_OPTION) if requested_mark in ("", None, "skip"): mark = MARK_GEN.skip elif requested_mark == "xfail": mark = MARK_GEN.xfail(run=False) + elif requested_mark == "fail_at_collect": + f_name = func.__name__ + _, lineno = getfslineno(func) + raise Collector.CollectError( + "Empty parameter set in '%s' at line %d" % (f_name, lineno) + ) else: raise LookupError(requested_mark) fs, lineno = getfslineno(func) diff --git a/testing/test_mark.py b/testing/test_mark.py index 9dad7a165..baf1d6f40 100644 --- a/testing/test_mark.py +++ b/testing/test_mark.py @@ -13,7 +13,7 @@ from _pytest.mark import ( transfer_markers, EMPTY_PARAMETERSET_OPTION, ) -from _pytest.nodes import Node +from _pytest.nodes import Node, Collector ignore_markinfo = pytest.mark.filterwarnings( "ignore:MarkInfo objects:pytest.RemovedInPytest4Warning" @@ -1091,7 +1091,14 @@ class TestMarkDecorator(object): @pytest.mark.parametrize("mark", [None, "", "skip", "xfail"]) def test_parameterset_for_parametrize_marks(testdir, mark): if mark is not None: - testdir.makeini("[pytest]\n{}={}".format(EMPTY_PARAMETERSET_OPTION, mark)) + testdir.makeini( + """ + [pytest] + {}={} + """.format( + EMPTY_PARAMETERSET_OPTION, mark + ) + ) config = testdir.parseconfig() from _pytest.mark import pytest_configure, get_empty_parameterset_mark @@ -1107,6 +1114,34 @@ def test_parameterset_for_parametrize_marks(testdir, mark): assert result_mark.kwargs.get("run") is False +def test_parameterset_for_fail_at_collect(testdir): + testdir.makeini( + """ + [pytest] + {}=fail_at_collect + """.format( + EMPTY_PARAMETERSET_OPTION + ) + ) + + config = testdir.parseconfig() + from _pytest.mark import pytest_configure, get_empty_parameterset_mark + from _pytest.compat import getfslineno + + pytest_configure(config) + + test_func = all + func_name = test_func.__name__ + _, func_lineno = getfslineno(test_func) + expected_errmsg = r"Empty parameter set in '%s' at line %d" % ( + func_name, + func_lineno, + ) + + with pytest.raises(Collector.CollectError, match=expected_errmsg): + get_empty_parameterset_mark(config, ["a"], test_func) + + def test_parameterset_for_parametrize_bad_markname(testdir): with pytest.raises(pytest.UsageError): test_parameterset_for_parametrize_marks(testdir, "bad") From 4a9f468aac312bde4c8dd499c3aeb14c02e9087c Mon Sep 17 00:00:00 2001 From: Andrea Cimatoribus Date: Wed, 12 Sep 2018 12:18:44 +0200 Subject: [PATCH 011/105] Update documentation --- doc/en/reference.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/doc/en/reference.rst b/doc/en/reference.rst index 52d83cf6e..f6a204fff 100644 --- a/doc/en/reference.rst +++ b/doc/en/reference.rst @@ -976,6 +976,7 @@ passed multiple times. The expected format is ``name=value``. For example:: * ``skip`` skips tests with an empty parameterset (default) * ``xfail`` marks tests with an empty parameterset as xfail(run=False) + * ``fail_at_collect`` raises an exception if parametrize collects an empty parameter set .. code-block:: ini From 913c07e41418c123228ad2ed50803b328f7a177f Mon Sep 17 00:00:00 2001 From: Andrea Cimatoribus Date: Wed, 12 Sep 2018 12:15:36 +0200 Subject: [PATCH 012/105] Add changelog file and new author --- AUTHORS | 1 + changelog/3849.feature.rst | 1 + 2 files changed, 2 insertions(+) create mode 100644 changelog/3849.feature.rst diff --git a/AUTHORS b/AUTHORS index 2d59a1b0f..9b4b8b8cc 100644 --- a/AUTHORS +++ b/AUTHORS @@ -13,6 +13,7 @@ Alexei Kozlenok Anatoly Bubenkoff Anders Hovmöller Andras Tim +Andrea Cimatoribus Andreas Zeidler Andrzej Ostrowski Andy Freeland diff --git a/changelog/3849.feature.rst b/changelog/3849.feature.rst new file mode 100644 index 000000000..26cbfe7b4 --- /dev/null +++ b/changelog/3849.feature.rst @@ -0,0 +1 @@ +Add ``empty_parameter_set_mark=fail_at_collect`` ini option for raising an exception when parametrize collects an empty set. From e967d4587a771106353232c410d5d2644d161ebe Mon Sep 17 00:00:00 2001 From: Thomas Hisch Date: Sat, 15 Sep 2018 01:41:07 +0200 Subject: [PATCH 013/105] Add support for logging in collection-phase The logging plugin does not output log messages generated during the collection-phase when live-logging is enabled. This fixes this. Fixes #3964 --- changelog/3964.rst | 2 ++ src/_pytest/logging.py | 41 ++++++++++++++++++++----------- testing/logging/test_reporting.py | 20 +++++++++++++++ 3 files changed, 48 insertions(+), 15 deletions(-) create mode 100644 changelog/3964.rst diff --git a/changelog/3964.rst b/changelog/3964.rst new file mode 100644 index 000000000..79ecd62a1 --- /dev/null +++ b/changelog/3964.rst @@ -0,0 +1,2 @@ +Log messages generated in the collection phase are now shown when +live-logging is enabled. diff --git a/src/_pytest/logging.py b/src/_pytest/logging.py index c9c65c4c1..f4a4801b8 100644 --- a/src/_pytest/logging.py +++ b/src/_pytest/logging.py @@ -2,7 +2,7 @@ from __future__ import absolute_import, division, print_function import logging -from contextlib import closing, contextmanager +from contextlib import contextmanager import re import six @@ -414,7 +414,6 @@ class LoggingPlugin(object): else: self.log_file_handler = None - # initialized during pytest_runtestloop self.log_cli_handler = None def _log_cli_enabled(self): @@ -425,6 +424,22 @@ class LoggingPlugin(object): "--log-cli-level" ) is not None or self._config.getini("log_cli") + @pytest.hookimpl(hookwrapper=True, tryfirst=True) + def pytest_collection(self): + # This has to be called before the first log message is logged, + # so we can access the terminal reporter plugin. + self._setup_cli_logging() + + with self.live_logs_context(): + if self.log_cli_handler: + self.log_cli_handler.set_when("collection") + + if self.log_file_handler is not None: + with catching_logs(self.log_file_handler, level=self.log_file_level): + yield # run all the tests + else: + yield + @contextmanager def _runtest_for(self, item, when): """Implements the internals of pytest_runtest_xxx() hook.""" @@ -484,22 +499,15 @@ class LoggingPlugin(object): @pytest.hookimpl(hookwrapper=True) def pytest_runtestloop(self, session): """Runs all collected test items.""" - self._setup_cli_logging() - with self.live_logs_context: + with self.live_logs_context(): if self.log_file_handler is not None: - with closing(self.log_file_handler): - with catching_logs( - self.log_file_handler, level=self.log_file_level - ): - yield # run all the tests + with catching_logs(self.log_file_handler, level=self.log_file_level): + yield # run all the tests else: yield # run all the tests def _setup_cli_logging(self): - """Sets up the handler and logger for the Live Logs feature, if enabled. - - This must be done right before starting the loop so we can access the terminal reporter plugin. - """ + """Sets up the handler and logger for the Live Logs feature, if enabled.""" terminal_reporter = self._config.pluginmanager.get_plugin("terminalreporter") if self._log_cli_enabled() and terminal_reporter is not None: capture_manager = self._config.pluginmanager.get_plugin("capturemanager") @@ -529,11 +537,14 @@ class LoggingPlugin(object): self._config, "log_cli_level", "log_level" ) self.log_cli_handler = log_cli_handler - self.live_logs_context = catching_logs( + self.live_logs_context = lambda: catching_logs( log_cli_handler, formatter=log_cli_formatter, level=log_cli_level ) else: - self.live_logs_context = dummy_context_manager() + self.live_logs_context = lambda: dummy_context_manager() + # Note that the lambda for the live_logs_context is needed because + # live_logs_context can otherwise not be entered multiple times due + # to limitations of contextlib.contextmanager class _LiveLoggingStreamHandler(logging.StreamHandler): diff --git a/testing/logging/test_reporting.py b/testing/logging/test_reporting.py index 363982cf9..085df1ebc 100644 --- a/testing/logging/test_reporting.py +++ b/testing/logging/test_reporting.py @@ -908,3 +908,23 @@ def test_live_logging_suspends_capture(has_capture_manager, request): else: assert MockCaptureManager.calls == [] assert out_file.getvalue() == "\nsome message\n" + + +def test_collection_live_logging(testdir): + testdir.makepyfile( + """ + import logging + + logging.getLogger().info("Normal message") + """ + ) + + result = testdir.runpytest("--log-cli-level=INFO") + result.stdout.fnmatch_lines( + [ + "collecting*", + "*--- live log collection ---*", + "*Normal message*", + "collected 0 items", + ] + ) From d1a3aa7b2bb0919348ba6a38a2c5ea99c8c3b118 Mon Sep 17 00:00:00 2001 From: Thomas Hisch Date: Tue, 18 Sep 2018 21:31:20 +0200 Subject: [PATCH 014/105] Update 3964.rst --- changelog/3964.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/changelog/3964.rst b/changelog/3964.rst index 79ecd62a1..37788c6f7 100644 --- a/changelog/3964.rst +++ b/changelog/3964.rst @@ -1,2 +1,2 @@ -Log messages generated in the collection phase are now shown when -live-logging is enabled. +Log messages generated in the collection phase are shown when +live-logging is enabled and/or when they are logged to a file. From 048342817b587ef0839331ca55a8f3f479a0d675 Mon Sep 17 00:00:00 2001 From: Thomas Hisch Date: Tue, 18 Sep 2018 21:47:42 +0200 Subject: [PATCH 015/105] Add testcase for logging to file --- testing/logging/test_reporting.py | 38 +++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/testing/logging/test_reporting.py b/testing/logging/test_reporting.py index 085df1ebc..498b4c5bd 100644 --- a/testing/logging/test_reporting.py +++ b/testing/logging/test_reporting.py @@ -928,3 +928,41 @@ def test_collection_live_logging(testdir): "collected 0 items", ] ) + + +def test_collection_logging_to_file(testdir): + log_file = testdir.tmpdir.join("pytest.log").strpath + + testdir.makeini( + """ + [pytest] + log_file={} + log_file_level = INFO + """.format( + log_file + ) + ) + + testdir.makepyfile( + """ + import logging + + logging.getLogger().info("Normal message") + + def test_simple(): + logging.getLogger().debug("debug message in test_simple") + logging.getLogger().info("info message in test_simple") + """ + ) + + result = testdir.runpytest() + + assert "--- live log collection ---" not in result.stdout.str() + + assert result.ret == 0 + assert os.path.isfile(log_file) + with open(log_file, encoding="utf-8") as rfh: + contents = rfh.read() + assert "Normal message" in contents + assert "debug message in test_simple" not in contents + assert "info message in test_simple" in contents From 18cc74b8d0cc788f0162775412a618477571ad35 Mon Sep 17 00:00:00 2001 From: Thomas Hisch Date: Wed, 19 Sep 2018 15:18:37 +0200 Subject: [PATCH 016/105] Remove useless comment --- src/_pytest/logging.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/_pytest/logging.py b/src/_pytest/logging.py index f4a4801b8..ba55ebb42 100644 --- a/src/_pytest/logging.py +++ b/src/_pytest/logging.py @@ -436,7 +436,7 @@ class LoggingPlugin(object): if self.log_file_handler is not None: with catching_logs(self.log_file_handler, level=self.log_file_level): - yield # run all the tests + yield else: yield From c30184709d0077843195b083fc9b1256f2e8a0d5 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Sat, 22 Sep 2018 08:27:53 -0300 Subject: [PATCH 017/105] Show deprecation warnings even if filters are customized Fix #4013 --- changelog/4013.feature.rst | 2 ++ doc/en/warnings.rst | 20 +++++++++++++------- src/_pytest/warnings.py | 12 +++++------- testing/test_warnings.py | 26 ++++++++++++++++++++++---- 4 files changed, 42 insertions(+), 18 deletions(-) create mode 100644 changelog/4013.feature.rst diff --git a/changelog/4013.feature.rst b/changelog/4013.feature.rst new file mode 100644 index 000000000..84c3ab79d --- /dev/null +++ b/changelog/4013.feature.rst @@ -0,0 +1,2 @@ +Deprecation warnings are now shown even if you customize the warnings filters yourself. In the previous version +any customization would override pytest's filters and deprecation warnings would fall back to being hidden by default. diff --git a/doc/en/warnings.rst b/doc/en/warnings.rst index 1f0c3bf97..07aec9855 100644 --- a/doc/en/warnings.rst +++ b/doc/en/warnings.rst @@ -101,22 +101,28 @@ DeprecationWarning and PendingDeprecationWarning ------------------------------------------------ .. versionadded:: 3.8 +.. versionchanged:: 3.9 -By default pytest will display ``DeprecationWarning`` and ``PendingDeprecationWarning`` if no other warning filters -are configured. +By default pytest will display ``DeprecationWarning`` and ``PendingDeprecationWarning``. -To disable showing ``DeprecationWarning`` and ``PendingDeprecationWarning`` warnings, you might define any warnings -filter either in the command-line or in the ini file, or you can use: +Sometimes it is useful to hide some specific deprecation warnings that happen in code that you have no control over +(such as third-party libraries), in which case you might use the standard warning filters options (ini or marks). +For example: .. code-block:: ini [pytest] filterwarnings = - ignore::DeprecationWarning - ignore::PendingDeprecationWarning + ignore:.*U.*mode is deprecated:DeprecationWarning + .. note:: - This makes pytest more compliant with `PEP-0506 `_ which suggests that those warnings should + If warnings are configured at the interpreter level, using + the `PYTHONWARNINGS `_ environment variable or the + ``-W`` command-line option, pytest will not configure any filters by default. + +.. note:: + This feature makes pytest more compliant with `PEP-0506 `_ which suggests that those warnings should be shown by default by test runners, but pytest doesn't follow ``PEP-0506`` completely because resetting all warning filters like suggested in the PEP will break existing test suites that configure warning filters themselves by calling ``warnings.simplefilter`` (see issue `#2430 `_ diff --git a/src/_pytest/warnings.py b/src/_pytest/warnings.py index fb303a20f..5574eee8e 100644 --- a/src/_pytest/warnings.py +++ b/src/_pytest/warnings.py @@ -70,7 +70,11 @@ def catch_warnings_for_item(config, ihook, when, item): cmdline_filters = config.getoption("pythonwarnings") or [] inifilters = config.getini("filterwarnings") with warnings.catch_warnings(record=True) as log: - filters_configured = bool(cmdline_filters or inifilters or sys.warnoptions) + + if not sys.warnoptions: + # if user is not explicitly configuring warning filters, show deprecation warnings by default (#2908) + warnings.filterwarnings("always", category=DeprecationWarning) + warnings.filterwarnings("always", category=PendingDeprecationWarning) # filters should have this precedence: mark, cmdline options, ini # filters should be applied in the inverse order of precedence @@ -84,12 +88,6 @@ def catch_warnings_for_item(config, ihook, when, item): for mark in item.iter_markers(name="filterwarnings"): for arg in mark.args: _setoption(warnings, arg) - filters_configured = True - - if not filters_configured: - # if user is not explicitly configuring warning filters, show deprecation warnings by default (#2908) - warnings.filterwarnings("always", category=DeprecationWarning) - warnings.filterwarnings("always", category=PendingDeprecationWarning) yield diff --git a/testing/test_warnings.py b/testing/test_warnings.py index 975605b5e..7825f2167 100644 --- a/testing/test_warnings.py +++ b/testing/test_warnings.py @@ -495,8 +495,18 @@ class TestDeprecationWarningsByDefault: ) ) - def test_shown_by_default(self, testdir): + @pytest.mark.parametrize("customize_filters", [True, False]) + def test_shown_by_default(self, testdir, customize_filters): + """Show deprecation warnings by default, even if user has customized the warnings filters (#4013).""" self.create_file(testdir) + if customize_filters: + testdir.makeini( + """ + [pytest] + filterwarnings = + once::UserWarning + """ + ) result = testdir.runpytest_subprocess() result.stdout.fnmatch_lines( [ @@ -512,7 +522,9 @@ class TestDeprecationWarningsByDefault: testdir.makeini( """ [pytest] - filterwarnings = once::UserWarning + filterwarnings = + ignore::DeprecationWarning + ignore::PendingDeprecationWarning """ ) result = testdir.runpytest_subprocess() @@ -523,7 +535,8 @@ class TestDeprecationWarningsByDefault: be displayed normally. """ self.create_file( - testdir, mark='@pytest.mark.filterwarnings("once::UserWarning")' + testdir, + mark='@pytest.mark.filterwarnings("ignore::PendingDeprecationWarning")', ) result = testdir.runpytest_subprocess() result.stdout.fnmatch_lines( @@ -536,7 +549,12 @@ class TestDeprecationWarningsByDefault: def test_hidden_by_cmdline(self, testdir): self.create_file(testdir) - result = testdir.runpytest_subprocess("-W", "once::UserWarning") + result = testdir.runpytest_subprocess( + "-W", + "ignore::DeprecationWarning", + "-W", + "ignore::PendingDeprecationWarning", + ) assert WARNINGS_SUMMARY_HEADER not in result.stdout.str() def test_hidden_by_system(self, testdir, monkeypatch): From fcc5b6d60407d13b068e84a33744844cc7010a1c Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Sat, 22 Sep 2018 18:43:22 -0300 Subject: [PATCH 018/105] Add "deprecation" to possible changelog entries in pre-commit --- .pre-commit-config.yaml | 2 +- changelog/{3964.rst => 3964.feature.rst} | 0 2 files changed, 1 insertion(+), 1 deletion(-) rename changelog/{3964.rst => 3964.feature.rst} (100%) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index f0340fb9d..5a1dd5ee6 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -41,5 +41,5 @@ repos: name: changelog filenames language: fail entry: 'changelog files must be named ####.(feature|bugfix|doc|removal|vendor|trivial).rst' - exclude: changelog/(\d+\.(feature|bugfix|doc|removal|vendor|trivial).rst|README.rst|_template.rst) + exclude: changelog/(\d+\.(feature|bugfix|doc|deprecation|removal|vendor|trivial).rst|README.rst|_template.rst) files: ^changelog/ diff --git a/changelog/3964.rst b/changelog/3964.feature.rst similarity index 100% rename from changelog/3964.rst rename to changelog/3964.feature.rst From 0d04aa7c59f0aa295ebbc0b85410775c1ba972d5 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Sat, 22 Sep 2018 18:38:50 -0300 Subject: [PATCH 019/105] Add 3.9 deprecated features to deprecations.rst --- doc/en/deprecations.rst | 61 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 61 insertions(+) diff --git a/doc/en/deprecations.rst b/doc/en/deprecations.rst index ee6c99193..3c35fbf4e 100644 --- a/doc/en/deprecations.rst +++ b/doc/en/deprecations.rst @@ -14,6 +14,67 @@ Below is a complete list of all pytest features which are considered deprecated. :class:`_pytest.warning_types.PytestWarning` or subclasses, which can be filtered using :ref:`standard warning filters `. +Internal classes accessed through ``Node`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. deprecated:: 3.9 + +Access of ``Module``, ``Function``, ``Class``, ``Instance``, ``File`` and ``Item`` through ``Node`` instances now issue +this warning:: + + usage of Function.Module is deprecated, please use pytest.Module instead + +Users should just ``import pytest`` and access those objects using the ``pytest`` module. + +This has been documented as deprecated for years, but only now we are actually emitting deprecation warnings. + +``cached_setup`` +~~~~~~~~~~~~~~~~ + +.. deprecated:: 3.9 + +``request.cached_setup`` was the precursor of the setup/teardown mechanism available to fixtures. + +Example: + +.. code-block:: python + + @pytest.fixture + def db_session(): + return request.cached_setup( + setup=Session.create, teardown=lambda session: session.close(), scope="module" + ) + +This should be updated to make use of standard fixture mechanisms: + +.. code-block:: python + + @pytest.fixture(scope="module") + def db_session(): + session = Session.create() + yield session + session.close() + + +You can consult `funcarg comparision section in the docs `_ for +more information. + +This has been documented as deprecated for years, but only now we are actually emitting deprecation warnings. + + +Using ``Class`` in custom Collectors +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. deprecated:: 3.9 + +Using objects named ``"Class"`` as a way to customize the type of nodes that are collected in ``Collector`` +subclasses has been deprecated. Users instead should use ``pytest_collect_make_item`` to customize node types during +collection. + +This issue should affect only advanced plugins who create new collection types, so if you see this warning +message please contact the authors so they can change the code. + + ``Config.warn`` and ``Node.warn`` ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ From 96b2ae66549b74b4f838e7f4169ae376e9528b52 Mon Sep 17 00:00:00 2001 From: Kyle Altendorf Date: Wed, 3 Oct 2018 23:50:42 -0400 Subject: [PATCH 020/105] Initial pass at timeout for subprocessing pytest pytest-dev/pytest#4073 --- setup.py | 1 + src/_pytest/pytester.py | 36 +++++++++++++++++++++++++++++++++--- testing/test_pytester.py | 18 ++++++++++++++++++ 3 files changed, 52 insertions(+), 3 deletions(-) diff --git a/setup.py b/setup.py index 4c12fbfcc..e65659825 100644 --- a/setup.py +++ b/setup.py @@ -65,6 +65,7 @@ def main(): "attrs>=17.4.0", "more-itertools>=4.0.0", "atomicwrites>=1.0", + "monotonic", ] # if _PYTEST_SETUP_SKIP_PLUGGY_DEP is set, skip installing pluggy; # used by tox.ini to test with pluggy master diff --git a/src/_pytest/pytester.py b/src/_pytest/pytester.py index a50999172..da0ac1868 100644 --- a/src/_pytest/pytester.py +++ b/src/_pytest/pytester.py @@ -3,6 +3,7 @@ from __future__ import absolute_import, division, print_function import codecs import gc +import monotonic import os import platform import re @@ -482,6 +483,9 @@ class Testdir(object): """ + class TimeoutExpired(Exception): + pass + def __init__(self, request, tmpdir_factory): self.request = request self._mod_collections = WeakKeyDictionary() @@ -1039,7 +1043,7 @@ class Testdir(object): return popen - def run(self, *cmdargs): + def run(self, *cmdargs, **kwargs): """Run a command with arguments. Run a process using subprocess.Popen saving the stdout and stderr. @@ -1061,7 +1065,27 @@ class Testdir(object): popen = self.popen( cmdargs, stdout=f1, stderr=f2, close_fds=(sys.platform != "win32") ) - ret = popen.wait() + timeout = kwargs.get('timeout') + if timeout is None: + ret = popen.wait() + elif six.PY3: + try: + ret = popen.wait(timeout) + except subprocess.TimeoutExpired: + raise self.TimeoutExpired + else: + end = monotonic.monotonic() + timeout + + while True: + ret = popen.poll() + if ret is not None: + break + + remaining = end - monotonic.monotonic() + if remaining <= 0: + raise self.TimeoutExpired() + + time.sleep(remaining * 0.9) finally: f1.close() f2.close() @@ -1119,7 +1143,13 @@ class Testdir(object): if plugins: args = ("-p", plugins[0]) + args args = self._getpytestargs() + args - return self.run(*args) + + if "timeout" in kwargs: + timeout = {"timeout": kwargs["timeout"]} + else: + timeout = {} + + return self.run(*args, **timeout) def spawn_pytest(self, string, expect_timeout=10.0): """Run pytest using pexpect. diff --git a/testing/test_pytester.py b/testing/test_pytester.py index c5a64b7bd..9ddbc1380 100644 --- a/testing/test_pytester.py +++ b/testing/test_pytester.py @@ -3,6 +3,7 @@ from __future__ import absolute_import, division, print_function import os import py.path import pytest +import subprocess import sys import _pytest.pytester as pytester from _pytest.pytester import HookRecorder @@ -401,3 +402,20 @@ def test_testdir_subprocess(testdir): def test_unicode_args(testdir): result = testdir.runpytest("-k", u"💩") assert result.ret == EXIT_NOTESTSCOLLECTED + + +def test_testdir_run_no_timeout(testdir): + testfile = testdir.makepyfile("def test_no_timeout(): pass") + assert testdir.runpytest_subprocess(testfile).ret == EXIT_OK + + +def test_testdir_run_timeout_expires(testdir): + testfile = testdir.makepyfile( + """ + import time + + def test_timeout(): + time.sleep(10)""" + ) + with pytest.raises(testdir.TimeoutExpired): + testdir.runpytest_subprocess(testfile, timeout=0.5) From 870a93c37b136d80406ce117b27b5d0f9631e1ee Mon Sep 17 00:00:00 2001 From: Kyle Altendorf Date: Thu, 4 Oct 2018 01:02:58 -0400 Subject: [PATCH 021/105] Actually construct TimeoutExpired --- src/_pytest/pytester.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/_pytest/pytester.py b/src/_pytest/pytester.py index da0ac1868..9591f3b52 100644 --- a/src/_pytest/pytester.py +++ b/src/_pytest/pytester.py @@ -1072,7 +1072,7 @@ class Testdir(object): try: ret = popen.wait(timeout) except subprocess.TimeoutExpired: - raise self.TimeoutExpired + raise self.TimeoutExpired() else: end = monotonic.monotonic() + timeout From fe7050ba004527edce5dabfb983d7efcf4d7dfa1 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Thu, 4 Oct 2018 18:45:30 -0300 Subject: [PATCH 022/105] Fix lint --- src/_pytest/pytester.py | 2 +- testing/test_pytester.py | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/src/_pytest/pytester.py b/src/_pytest/pytester.py index 9591f3b52..15638e42b 100644 --- a/src/_pytest/pytester.py +++ b/src/_pytest/pytester.py @@ -1065,7 +1065,7 @@ class Testdir(object): popen = self.popen( cmdargs, stdout=f1, stderr=f2, close_fds=(sys.platform != "win32") ) - timeout = kwargs.get('timeout') + timeout = kwargs.get("timeout") if timeout is None: ret = popen.wait() elif six.PY3: diff --git a/testing/test_pytester.py b/testing/test_pytester.py index 9ddbc1380..ebc699cc5 100644 --- a/testing/test_pytester.py +++ b/testing/test_pytester.py @@ -3,7 +3,6 @@ from __future__ import absolute_import, division, print_function import os import py.path import pytest -import subprocess import sys import _pytest.pytester as pytester from _pytest.pytester import HookRecorder @@ -413,7 +412,7 @@ def test_testdir_run_timeout_expires(testdir): testfile = testdir.makepyfile( """ import time - + def test_timeout(): time.sleep(10)""" ) From d2906950ce8afe2f19b56a5046a2274a2c7d65f6 Mon Sep 17 00:00:00 2001 From: Kyle Altendorf Date: Thu, 4 Oct 2018 21:26:08 -0400 Subject: [PATCH 023/105] monotonic.monotonic() -> time.time() --- setup.py | 1 - src/_pytest/pytester.py | 5 ++--- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/setup.py b/setup.py index e65659825..4c12fbfcc 100644 --- a/setup.py +++ b/setup.py @@ -65,7 +65,6 @@ def main(): "attrs>=17.4.0", "more-itertools>=4.0.0", "atomicwrites>=1.0", - "monotonic", ] # if _PYTEST_SETUP_SKIP_PLUGGY_DEP is set, skip installing pluggy; # used by tox.ini to test with pluggy master diff --git a/src/_pytest/pytester.py b/src/_pytest/pytester.py index 15638e42b..1b836e546 100644 --- a/src/_pytest/pytester.py +++ b/src/_pytest/pytester.py @@ -3,7 +3,6 @@ from __future__ import absolute_import, division, print_function import codecs import gc -import monotonic import os import platform import re @@ -1074,14 +1073,14 @@ class Testdir(object): except subprocess.TimeoutExpired: raise self.TimeoutExpired() else: - end = monotonic.monotonic() + timeout + end = time.time() + timeout while True: ret = popen.poll() if ret is not None: break - remaining = end - monotonic.monotonic() + remaining = end - time.time() if remaining <= 0: raise self.TimeoutExpired() From d5e5433553a52d93654b863669593f68a6a7556e Mon Sep 17 00:00:00 2001 From: Kyle Altendorf Date: Thu, 4 Oct 2018 21:43:41 -0400 Subject: [PATCH 024/105] Add descriptive message for timeout --- src/_pytest/pytester.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/_pytest/pytester.py b/src/_pytest/pytester.py index 1b836e546..7037b41c7 100644 --- a/src/_pytest/pytester.py +++ b/src/_pytest/pytester.py @@ -1065,13 +1065,19 @@ class Testdir(object): cmdargs, stdout=f1, stderr=f2, close_fds=(sys.platform != "win32") ) timeout = kwargs.get("timeout") + + timeout_message = ( + "{seconds} second timeout expired running:" + " {command}".format(seconds=timeout, command=cmdargs) + ) + if timeout is None: ret = popen.wait() elif six.PY3: try: ret = popen.wait(timeout) except subprocess.TimeoutExpired: - raise self.TimeoutExpired() + raise self.TimeoutExpired(timeout_message) else: end = time.time() + timeout @@ -1082,7 +1088,7 @@ class Testdir(object): remaining = end - time.time() if remaining <= 0: - raise self.TimeoutExpired() + raise self.TimeoutExpired(timeout_message) time.sleep(remaining * 0.9) finally: From 33f0338eeb14e4b154122447d73f161b536d70f1 Mon Sep 17 00:00:00 2001 From: Kyle Altendorf Date: Thu, 4 Oct 2018 21:57:29 -0400 Subject: [PATCH 025/105] kill and wait for subprocess before raising TimeoutExpired --- src/_pytest/pytester.py | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/src/_pytest/pytester.py b/src/_pytest/pytester.py index 7037b41c7..d7ad6e3b0 100644 --- a/src/_pytest/pytester.py +++ b/src/_pytest/pytester.py @@ -1066,10 +1066,15 @@ class Testdir(object): ) timeout = kwargs.get("timeout") - timeout_message = ( - "{seconds} second timeout expired running:" - " {command}".format(seconds=timeout, command=cmdargs) - ) + def handle_timeout(): + timeout_message = ( + "{seconds} second timeout expired running:" + " {command}".format(seconds=timeout, command=cmdargs) + ) + + popen.kill() + popen.wait() + raise self.TimeoutExpired(timeout_message) if timeout is None: ret = popen.wait() @@ -1077,7 +1082,7 @@ class Testdir(object): try: ret = popen.wait(timeout) except subprocess.TimeoutExpired: - raise self.TimeoutExpired(timeout_message) + handle_timeout() else: end = time.time() + timeout @@ -1088,7 +1093,7 @@ class Testdir(object): remaining = end - time.time() if remaining <= 0: - raise self.TimeoutExpired(timeout_message) + handle_timeout() time.sleep(remaining * 0.9) finally: From dcd635ba0cbba40efce1947071cc0cfb7830952e Mon Sep 17 00:00:00 2001 From: Kyle Altendorf Date: Thu, 4 Oct 2018 22:11:02 -0400 Subject: [PATCH 026/105] Correct timeout to check every so often --- src/_pytest/pytester.py | 4 +++- testing/test_pytester.py | 13 +++++++++++++ 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/src/_pytest/pytester.py b/src/_pytest/pytester.py index d7ad6e3b0..73265f52b 100644 --- a/src/_pytest/pytester.py +++ b/src/_pytest/pytester.py @@ -1086,6 +1086,8 @@ class Testdir(object): else: end = time.time() + timeout + resolution = min(0.1, timeout / 10) + while True: ret = popen.poll() if ret is not None: @@ -1095,7 +1097,7 @@ class Testdir(object): if remaining <= 0: handle_timeout() - time.sleep(remaining * 0.9) + time.sleep(resolution) finally: f1.close() f2.close() diff --git a/testing/test_pytester.py b/testing/test_pytester.py index ebc699cc5..de5145250 100644 --- a/testing/test_pytester.py +++ b/testing/test_pytester.py @@ -4,6 +4,7 @@ import os import py.path import pytest import sys +import time import _pytest.pytester as pytester from _pytest.pytester import HookRecorder from _pytest.pytester import CwdSnapshot, SysModulesSnapshot, SysPathsSnapshot @@ -408,6 +409,18 @@ def test_testdir_run_no_timeout(testdir): assert testdir.runpytest_subprocess(testfile).ret == EXIT_OK +def test_testdir_run_with_timeout(testdir): + testfile = testdir.makepyfile("def test_no_timeout(): pass") + + start = time.time() + result = testdir.runpytest_subprocess(testfile, timeout=10) + end = time.time() + duration = end - start + + assert result.ret == EXIT_OK + assert duration < 1 + + def test_testdir_run_timeout_expires(testdir): testfile = testdir.makepyfile( """ From 0d095fc978dde9994d81a7fb40d5bc5551d32283 Mon Sep 17 00:00:00 2001 From: Kyle Altendorf Date: Thu, 4 Oct 2018 23:00:17 -0400 Subject: [PATCH 027/105] Up timeout to 1 second for test --- testing/test_pytester.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/testing/test_pytester.py b/testing/test_pytester.py index de5145250..caca4f687 100644 --- a/testing/test_pytester.py +++ b/testing/test_pytester.py @@ -430,4 +430,4 @@ def test_testdir_run_timeout_expires(testdir): time.sleep(10)""" ) with pytest.raises(testdir.TimeoutExpired): - testdir.runpytest_subprocess(testfile, timeout=0.5) + testdir.runpytest_subprocess(testfile, timeout=1) From 900cef639710682c67a4ab88bd9e7666b14a9da2 Mon Sep 17 00:00:00 2001 From: Kyle Altendorf Date: Thu, 4 Oct 2018 23:11:26 -0400 Subject: [PATCH 028/105] Use signal.alarm() for py2 timeout --- src/_pytest/pytester.py | 35 +++++++++++++++++++++-------------- 1 file changed, 21 insertions(+), 14 deletions(-) diff --git a/src/_pytest/pytester.py b/src/_pytest/pytester.py index 73265f52b..cf50f9ec9 100644 --- a/src/_pytest/pytester.py +++ b/src/_pytest/pytester.py @@ -2,11 +2,13 @@ from __future__ import absolute_import, division, print_function import codecs +import contextlib import gc import os import platform import re import subprocess +import signal import six import sys import time @@ -1076,6 +1078,21 @@ class Testdir(object): popen.wait() raise self.TimeoutExpired(timeout_message) + @contextlib.contextmanager + def timeout_manager(handler, timeout): + original_handler = signal.getsignal(signal.SIGALRM) + if original_handler != signal.SIG_DFL: + # TODO: use an informative exception + raise Exception() + + signal.signal(signal.SIGALRM, handler) + signal.alarm(timeout) + + yield + + signal.alarm(0) + signal.signal(signal.SIGALRM, original_handler) + if timeout is None: ret = popen.wait() elif six.PY3: @@ -1084,20 +1101,10 @@ class Testdir(object): except subprocess.TimeoutExpired: handle_timeout() else: - end = time.time() + timeout - - resolution = min(0.1, timeout / 10) - - while True: - ret = popen.poll() - if ret is not None: - break - - remaining = end - time.time() - if remaining <= 0: - handle_timeout() - - time.sleep(resolution) + with timeout_manager( + handler=lambda _1, _2: handle_timeout(), timeout=timeout + ): + ret = popen.wait() finally: f1.close() f2.close() From dd225e1b9d17212dfa079c0266336d6fb40de592 Mon Sep 17 00:00:00 2001 From: Kyle Altendorf Date: Thu, 4 Oct 2018 23:15:30 -0400 Subject: [PATCH 029/105] Tidy getting of timeout from kwargs --- src/_pytest/pytester.py | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/src/_pytest/pytester.py b/src/_pytest/pytester.py index cf50f9ec9..3f4d43eb5 100644 --- a/src/_pytest/pytester.py +++ b/src/_pytest/pytester.py @@ -1163,12 +1163,7 @@ class Testdir(object): args = ("-p", plugins[0]) + args args = self._getpytestargs() + args - if "timeout" in kwargs: - timeout = {"timeout": kwargs["timeout"]} - else: - timeout = {} - - return self.run(*args, **timeout) + return self.run(*args, timeout=kwargs.get("timeout")) def spawn_pytest(self, string, expect_timeout=10.0): """Run pytest using pexpect. From dcf9eb01043fb4131cf7e8e7b402695d3f8bdecc Mon Sep 17 00:00:00 2001 From: Kyle Altendorf Date: Thu, 4 Oct 2018 23:25:43 -0400 Subject: [PATCH 030/105] Raise an exception on unexpected kwargs --- src/_pytest/pytester.py | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/src/_pytest/pytester.py b/src/_pytest/pytester.py index 3f4d43eb5..f16e5f19a 100644 --- a/src/_pytest/pytester.py +++ b/src/_pytest/pytester.py @@ -63,6 +63,11 @@ def pytest_configure(config): config.pluginmanager.register(checker) +def raise_on_kwargs(kwargs): + if kwargs: + raise TypeError("Unexpected arguments: {}".format(", ".join(sorted(kwargs)))) + + class LsofFdLeakChecker(object): def get_open_files(self): out = self._exec_lsof() @@ -1052,6 +1057,9 @@ class Testdir(object): Returns a :py:class:`RunResult`. """ + timeout = kwargs.pop("timeout", None) + raise_on_kwargs(kwargs) + cmdargs = [ str(arg) if isinstance(arg, py.path.local) else arg for arg in cmdargs ] @@ -1066,7 +1074,6 @@ class Testdir(object): popen = self.popen( cmdargs, stdout=f1, stderr=f2, close_fds=(sys.platform != "win32") ) - timeout = kwargs.get("timeout") def handle_timeout(): timeout_message = ( @@ -1154,6 +1161,9 @@ class Testdir(object): Returns a :py:class:`RunResult`. """ + timeout = kwargs.pop("timeout", None) + raise_on_kwargs(kwargs) + p = py.path.local.make_numbered_dir( prefix="runpytest-", keep=None, rootdir=self.tmpdir ) @@ -1163,7 +1173,7 @@ class Testdir(object): args = ("-p", plugins[0]) + args args = self._getpytestargs() + args - return self.run(*args, timeout=kwargs.get("timeout")) + return self.run(*args, timeout=timeout) def spawn_pytest(self, string, expect_timeout=10.0): """Run pytest using pexpect. From 5c38a5160d7878a12b459f8da6f441681ff9d59f Mon Sep 17 00:00:00 2001 From: Kyle Altendorf Date: Thu, 4 Oct 2018 23:33:38 -0400 Subject: [PATCH 031/105] Slight diff tidy --- src/_pytest/pytester.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/_pytest/pytester.py b/src/_pytest/pytester.py index f16e5f19a..d519162b1 100644 --- a/src/_pytest/pytester.py +++ b/src/_pytest/pytester.py @@ -1172,7 +1172,6 @@ class Testdir(object): if plugins: args = ("-p", plugins[0]) + args args = self._getpytestargs() + args - return self.run(*args, timeout=timeout) def spawn_pytest(self, string, expect_timeout=10.0): From f3a173b7369cbf5807dbc8b8c7d08189debb471f Mon Sep 17 00:00:00 2001 From: Kyle Altendorf Date: Fri, 5 Oct 2018 00:05:46 -0400 Subject: [PATCH 032/105] Revert "Use signal.alarm() for py2 timeout" This reverts commit 900cef639710682c67a4ab88bd9e7666b14a9da2. --- src/_pytest/pytester.py | 35 ++++++++++++++--------------------- 1 file changed, 14 insertions(+), 21 deletions(-) diff --git a/src/_pytest/pytester.py b/src/_pytest/pytester.py index d519162b1..a20c281e1 100644 --- a/src/_pytest/pytester.py +++ b/src/_pytest/pytester.py @@ -2,13 +2,11 @@ from __future__ import absolute_import, division, print_function import codecs -import contextlib import gc import os import platform import re import subprocess -import signal import six import sys import time @@ -1085,21 +1083,6 @@ class Testdir(object): popen.wait() raise self.TimeoutExpired(timeout_message) - @contextlib.contextmanager - def timeout_manager(handler, timeout): - original_handler = signal.getsignal(signal.SIGALRM) - if original_handler != signal.SIG_DFL: - # TODO: use an informative exception - raise Exception() - - signal.signal(signal.SIGALRM, handler) - signal.alarm(timeout) - - yield - - signal.alarm(0) - signal.signal(signal.SIGALRM, original_handler) - if timeout is None: ret = popen.wait() elif six.PY3: @@ -1108,10 +1091,20 @@ class Testdir(object): except subprocess.TimeoutExpired: handle_timeout() else: - with timeout_manager( - handler=lambda _1, _2: handle_timeout(), timeout=timeout - ): - ret = popen.wait() + end = time.time() + timeout + + resolution = min(0.1, timeout / 10) + + while True: + ret = popen.poll() + if ret is not None: + break + + remaining = end - time.time() + if remaining <= 0: + handle_timeout() + + time.sleep(resolution) finally: f1.close() f2.close() From 42422a7f629265bdcd1afb76a44911cb4673b95e Mon Sep 17 00:00:00 2001 From: Kyle Altendorf Date: Fri, 5 Oct 2018 00:30:25 -0400 Subject: [PATCH 033/105] Throw away arbitrary args to runpytest_subprocess() --- src/_pytest/pytester.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/_pytest/pytester.py b/src/_pytest/pytester.py index a20c281e1..8c283431a 100644 --- a/src/_pytest/pytester.py +++ b/src/_pytest/pytester.py @@ -1154,9 +1154,6 @@ class Testdir(object): Returns a :py:class:`RunResult`. """ - timeout = kwargs.pop("timeout", None) - raise_on_kwargs(kwargs) - p = py.path.local.make_numbered_dir( prefix="runpytest-", keep=None, rootdir=self.tmpdir ) @@ -1165,7 +1162,7 @@ class Testdir(object): if plugins: args = ("-p", plugins[0]) + args args = self._getpytestargs() + args - return self.run(*args, timeout=timeout) + return self.run(*args, timeout=kwargs.get("timeout")) def spawn_pytest(self, string, expect_timeout=10.0): """Run pytest using pexpect. From 8e0e862c84f44dfa3e429275b3a3a5d29b5836a8 Mon Sep 17 00:00:00 2001 From: Kyle Altendorf Date: Fri, 5 Oct 2018 01:38:01 -0400 Subject: [PATCH 034/105] Stretch out the time assertion for slow AppVeyor --- testing/test_pytester.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/testing/test_pytester.py b/testing/test_pytester.py index caca4f687..42f245524 100644 --- a/testing/test_pytester.py +++ b/testing/test_pytester.py @@ -418,7 +418,7 @@ def test_testdir_run_with_timeout(testdir): duration = end - start assert result.ret == EXIT_OK - assert duration < 1 + assert duration < 5 def test_testdir_run_timeout_expires(testdir): From ee64f1fb9f00a82e91055a3e70febfff643f016e Mon Sep 17 00:00:00 2001 From: Kyle Altendorf Date: Fri, 5 Oct 2018 09:02:53 -0400 Subject: [PATCH 035/105] Add changelog file for 4073 --- changelog/4073.feature.rst | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog/4073.feature.rst diff --git a/changelog/4073.feature.rst b/changelog/4073.feature.rst new file mode 100644 index 000000000..7ee46c60f --- /dev/null +++ b/changelog/4073.feature.rst @@ -0,0 +1 @@ +Allow specification of timeout for ``Testdir.runpytest_subprocess()`` and ``Testdir.run()``. \ No newline at end of file From ed5556bdac8b864937d5df2ea690573df27a4f83 Mon Sep 17 00:00:00 2001 From: Kyle Altendorf Date: Fri, 5 Oct 2018 10:00:52 -0400 Subject: [PATCH 036/105] Add to docstrings --- src/_pytest/pytester.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/_pytest/pytester.py b/src/_pytest/pytester.py index 8c283431a..603de4b65 100644 --- a/src/_pytest/pytester.py +++ b/src/_pytest/pytester.py @@ -1052,6 +1052,10 @@ class Testdir(object): Run a process using subprocess.Popen saving the stdout and stderr. + :param args: the sequence of arguments to pass to `subprocess.Popen()` + :param timeout: the period in seconds after which to timeout and raise + :py:class:`Testdir.TimeoutExpired` + Returns a :py:class:`RunResult`. """ @@ -1151,6 +1155,10 @@ class Testdir(object): with "runpytest-" so they do not conflict with the normal numbered pytest location for temporary files and directories. + :param args: the sequence of arguments to pass to the pytest subprocess + :param timeout: the period in seconds after which to timeout and raise + :py:class:`Testdir.TimeoutExpired` + Returns a :py:class:`RunResult`. """ From 20902deb75d7d84b24c9f640439a45625aaac0d6 Mon Sep 17 00:00:00 2001 From: Kyle Altendorf Date: Fri, 5 Oct 2018 10:02:31 -0400 Subject: [PATCH 037/105] Add Kyle Altendorf to AUTHORS --- AUTHORS | 1 + 1 file changed, 1 insertion(+) diff --git a/AUTHORS b/AUTHORS index d99261478..988d0e5fe 100644 --- a/AUTHORS +++ b/AUTHORS @@ -121,6 +121,7 @@ Katerina Koukiou Kevin Cox Kodi B. Arfer Kostis Anagnostopoulos +Kyle Altendorf Lawrence Mitchell Lee Kamentsky Lev Maximov From 5ecbb0acba66e56a88805bde53515b6dda1c5452 Mon Sep 17 00:00:00 2001 From: Kyle Altendorf Date: Fri, 5 Oct 2018 10:22:07 -0400 Subject: [PATCH 038/105] Correct new changelog to have newline at end --- changelog/4073.feature.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/changelog/4073.feature.rst b/changelog/4073.feature.rst index 7ee46c60f..5b0ed5927 100644 --- a/changelog/4073.feature.rst +++ b/changelog/4073.feature.rst @@ -1 +1 @@ -Allow specification of timeout for ``Testdir.runpytest_subprocess()`` and ``Testdir.run()``. \ No newline at end of file +Allow specification of timeout for ``Testdir.runpytest_subprocess()`` and ``Testdir.run()``. From 4b36f9aa64b90fde67db88d39c63d6c5e6116b9b Mon Sep 17 00:00:00 2001 From: Kyle Altendorf Date: Fri, 5 Oct 2018 16:46:24 -0400 Subject: [PATCH 039/105] Tidy timeout checking --- src/_pytest/pytester.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/_pytest/pytester.py b/src/_pytest/pytester.py index 603de4b65..8e00f0ebf 100644 --- a/src/_pytest/pytester.py +++ b/src/_pytest/pytester.py @@ -1104,8 +1104,7 @@ class Testdir(object): if ret is not None: break - remaining = end - time.time() - if remaining <= 0: + if time.time() > end: handle_timeout() time.sleep(resolution) From ccaec8d360e17e041a8f95e2a0942859615feae3 Mon Sep 17 00:00:00 2001 From: Kyle Altendorf Date: Sat, 6 Oct 2018 21:56:59 -0400 Subject: [PATCH 040/105] __tracebackhide__ = True --- src/_pytest/pytester.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/_pytest/pytester.py b/src/_pytest/pytester.py index 8e00f0ebf..31d9f7860 100644 --- a/src/_pytest/pytester.py +++ b/src/_pytest/pytester.py @@ -1059,6 +1059,8 @@ class Testdir(object): Returns a :py:class:`RunResult`. """ + __tracebackhide__ = True + timeout = kwargs.pop("timeout", None) raise_on_kwargs(kwargs) @@ -1078,6 +1080,8 @@ class Testdir(object): ) def handle_timeout(): + __tracebackhide__ = True + timeout_message = ( "{seconds} second timeout expired running:" " {command}".format(seconds=timeout, command=cmdargs) @@ -1161,6 +1165,8 @@ class Testdir(object): Returns a :py:class:`RunResult`. """ + __tracebackhide__ = True + p = py.path.local.make_numbered_dir( prefix="runpytest-", keep=None, rootdir=self.tmpdir ) From 48dcc6727421bed1f9fe335ba53fae775a8c2fcc Mon Sep 17 00:00:00 2001 From: Kyle Altendorf Date: Sat, 6 Oct 2018 22:02:33 -0400 Subject: [PATCH 041/105] Increase timeout in test_testdir_run_with_timeout to decrease false failures --- testing/test_pytester.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/testing/test_pytester.py b/testing/test_pytester.py index 42f245524..1c456e167 100644 --- a/testing/test_pytester.py +++ b/testing/test_pytester.py @@ -413,7 +413,7 @@ def test_testdir_run_with_timeout(testdir): testfile = testdir.makepyfile("def test_no_timeout(): pass") start = time.time() - result = testdir.runpytest_subprocess(testfile, timeout=10) + result = testdir.runpytest_subprocess(testfile, timeout=120) end = time.time() duration = end - start From 4071c8a4a8ff3b39c7a4f4461140527aea4a889a Mon Sep 17 00:00:00 2001 From: Kyle Altendorf Date: Mon, 8 Oct 2018 15:54:14 -0400 Subject: [PATCH 042/105] Correct timing in test_pytester.test_testdir_run_with_timeout() --- testing/test_pytester.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/testing/test_pytester.py b/testing/test_pytester.py index 1c456e167..879c2027d 100644 --- a/testing/test_pytester.py +++ b/testing/test_pytester.py @@ -412,13 +412,15 @@ def test_testdir_run_no_timeout(testdir): def test_testdir_run_with_timeout(testdir): testfile = testdir.makepyfile("def test_no_timeout(): pass") + timeout = 120 + start = time.time() - result = testdir.runpytest_subprocess(testfile, timeout=120) + result = testdir.runpytest_subprocess(testfile, timeout=timeout) end = time.time() duration = end - start assert result.ret == EXIT_OK - assert duration < 5 + assert duration < timeout def test_testdir_run_timeout_expires(testdir): From aeb92accb2521f1f1ccc4faaf9e6f6bd36e04d8b Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Wed, 10 Oct 2018 08:00:59 -0700 Subject: [PATCH 043/105] Implement pytest.deprecated_call with pytest.warns --- changelog/4102.feature.rst | 1 + src/_pytest/recwarn.py | 44 +++++--------------------------------- testing/test_recwarn.py | 21 ++++++++++++------ 3 files changed, 20 insertions(+), 46 deletions(-) create mode 100644 changelog/4102.feature.rst diff --git a/changelog/4102.feature.rst b/changelog/4102.feature.rst new file mode 100644 index 000000000..b7916c841 --- /dev/null +++ b/changelog/4102.feature.rst @@ -0,0 +1 @@ +Reimplement ``pytest.deprecated_call`` using ``pytest.warns`` so it supports the ``match='...'`` keyword argument. diff --git a/src/_pytest/recwarn.py b/src/_pytest/recwarn.py index 7a0586697..c0121f7c1 100644 --- a/src/_pytest/recwarn.py +++ b/src/_pytest/recwarn.py @@ -43,45 +43,10 @@ def deprecated_call(func=None, *args, **kwargs): in which case it will ensure calling ``func(*args, **kwargs)`` produces one of the warnings types above. """ - if not func: - return _DeprecatedCallContext() - else: - __tracebackhide__ = True - with _DeprecatedCallContext(): - return func(*args, **kwargs) - - -class _DeprecatedCallContext(object): - """Implements the logic to capture deprecation warnings as a context manager.""" - - def __enter__(self): - self._captured_categories = [] - self._old_warn = warnings.warn - self._old_warn_explicit = warnings.warn_explicit - warnings.warn_explicit = self._warn_explicit - warnings.warn = self._warn - - def _warn_explicit(self, message, category, *args, **kwargs): - self._captured_categories.append(category) - - def _warn(self, message, category=None, *args, **kwargs): - if isinstance(message, Warning): - self._captured_categories.append(message.__class__) - else: - self._captured_categories.append(category) - - def __exit__(self, exc_type, exc_val, exc_tb): - warnings.warn_explicit = self._old_warn_explicit - warnings.warn = self._old_warn - - if exc_type is None: - deprecation_categories = (DeprecationWarning, PendingDeprecationWarning) - if not any( - issubclass(c, deprecation_categories) for c in self._captured_categories - ): - __tracebackhide__ = True - msg = "Did not produce DeprecationWarning or PendingDeprecationWarning" - raise AssertionError(msg) + __tracebackhide__ = True + if func is not None: + args = (func,) + args + return warns((DeprecationWarning, PendingDeprecationWarning), *args, **kwargs) def warns(expected_warning, *args, **kwargs): @@ -116,6 +81,7 @@ def warns(expected_warning, *args, **kwargs): Failed: DID NOT WARN. No warnings of type ...UserWarning... was emitted... """ + __tracebackhide__ = True match_expr = None if not args: if "match" in kwargs: diff --git a/testing/test_recwarn.py b/testing/test_recwarn.py index 82bd66c55..897f4c5e8 100644 --- a/testing/test_recwarn.py +++ b/testing/test_recwarn.py @@ -76,9 +76,8 @@ class TestDeprecatedCall(object): ) def test_deprecated_call_raises(self): - with pytest.raises(AssertionError) as excinfo: + with pytest.raises(pytest.fail.Exception, match="No warnings of type"): pytest.deprecated_call(self.dep, 3, 5) - assert "Did not produce" in str(excinfo) def test_deprecated_call(self): pytest.deprecated_call(self.dep, 0, 5) @@ -100,7 +99,7 @@ class TestDeprecatedCall(object): assert warn_explicit is warnings.warn_explicit def test_deprecated_explicit_call_raises(self): - with pytest.raises(AssertionError): + with pytest.raises(pytest.fail.Exception): pytest.deprecated_call(self.dep_explicit, 3) def test_deprecated_explicit_call(self): @@ -116,8 +115,8 @@ class TestDeprecatedCall(object): def f(): pass - msg = "Did not produce DeprecationWarning or PendingDeprecationWarning" - with pytest.raises(AssertionError, match=msg): + msg = "No warnings of type (.*DeprecationWarning.*, .*PendingDeprecationWarning.*)" + with pytest.raises(pytest.fail.Exception, match=msg): if mode == "call": pytest.deprecated_call(f) else: @@ -179,12 +178,20 @@ class TestDeprecatedCall(object): def f(): warnings.warn(warning("hi")) - with pytest.raises(AssertionError): + with pytest.raises(pytest.fail.Exception): pytest.deprecated_call(f) - with pytest.raises(AssertionError): + with pytest.raises(pytest.fail.Exception): with pytest.deprecated_call(): f() + def test_deprecated_call_supports_match(self): + with pytest.deprecated_call(match=r"must be \d+$"): + warnings.warn("value must be 42", DeprecationWarning) + + with pytest.raises(pytest.fail.Exception): + with pytest.deprecated_call(match=r"must be \d+$"): + warnings.warn("this is not here", DeprecationWarning) + class TestWarns(object): def test_strings(self): From d93de6cc67b26c62490eb0e080934835d8c213ea Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Wed, 10 Oct 2018 18:14:47 +0200 Subject: [PATCH 044/105] Fix trailing whitespace in FixtureDef.__repr__ --- src/_pytest/fixtures.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/_pytest/fixtures.py b/src/_pytest/fixtures.py index 964b16e29..4b89b1cdb 100644 --- a/src/_pytest/fixtures.py +++ b/src/_pytest/fixtures.py @@ -899,7 +899,7 @@ class FixtureDef(object): return hook.pytest_fixture_setup(fixturedef=self, request=request) def __repr__(self): - return "" % ( + return "" % ( self.argname, self.scope, self.baseid, From e0f6fce9e9213d5beda7f54e800a72ac95cbb3b6 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Wed, 10 Oct 2018 09:36:19 -0700 Subject: [PATCH 045/105] In python2, display previously warned warnings --- changelog/4012.bugfix.rst | 1 + src/_pytest/recwarn.py | 13 +++++++++++++ testing/test_recwarn.py | 10 ++++++++++ 3 files changed, 24 insertions(+) create mode 100644 changelog/4012.bugfix.rst diff --git a/changelog/4012.bugfix.rst b/changelog/4012.bugfix.rst new file mode 100644 index 000000000..11d8deef7 --- /dev/null +++ b/changelog/4012.bugfix.rst @@ -0,0 +1 @@ +``pytest.warn`` will capture previously-warned warnings in python2. Previously they were never raised. diff --git a/src/_pytest/recwarn.py b/src/_pytest/recwarn.py index c0121f7c1..592abdf5b 100644 --- a/src/_pytest/recwarn.py +++ b/src/_pytest/recwarn.py @@ -149,12 +149,25 @@ class WarningsRecorder(warnings.catch_warnings): raise RuntimeError("Cannot enter %r twice" % self) self._list = super(WarningsRecorder, self).__enter__() warnings.simplefilter("always") + # python3 keeps track of a "filter version", when the filters are + # updated previously seen warnings can be re-warned. python2 has no + # concept of this so we must reset the warnings registry manually. + # trivial patching of `warnings.warn` seems to be enough somehow? + if six.PY2: + + def warn(*args, **kwargs): + return self._warn(*args, **kwargs) + + warnings.warn, self._warn = warn, warnings.warn return self def __exit__(self, *exc_info): if not self._entered: __tracebackhide__ = True raise RuntimeError("Cannot exit %r without entering first" % self) + # see above where `self.mp` is assigned + if six.PY2: + warnings.warn = self._warn super(WarningsRecorder, self).__exit__(*exc_info) diff --git a/testing/test_recwarn.py b/testing/test_recwarn.py index 897f4c5e8..3ae543248 100644 --- a/testing/test_recwarn.py +++ b/testing/test_recwarn.py @@ -350,3 +350,13 @@ class TestWarns(object): with pytest.warns(UserWarning, match=r"aaa"): warnings.warn("bbbbbbbbbb", UserWarning) warnings.warn("cccccccccc", UserWarning) + + @pytest.mark.filterwarnings("ignore") + def test_can_capture_previously_warned(self): + def f(): + warnings.warn(UserWarning("ohai")) + return 10 + + assert f() == 10 + assert pytest.warns(UserWarning, f) == 10 + assert pytest.warns(UserWarning, f) == 10 From b8fc3e569ae0997b081b4002f2d663e8f477ffaf Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Wed, 10 Oct 2018 18:38:13 +0200 Subject: [PATCH 046/105] pytester: fix __tracebackhide__ for {re_,fn}match_lines --- src/_pytest/pytester.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/_pytest/pytester.py b/src/_pytest/pytester.py index 31d9f7860..1e64c1747 100644 --- a/src/_pytest/pytester.py +++ b/src/_pytest/pytester.py @@ -1323,6 +1323,7 @@ class LineMatcher(object): matches and non-matches are also printed on stdout. """ + __tracebackhide__ = True self._match_lines(lines2, fnmatch, "fnmatch") def re_match_lines(self, lines2): @@ -1334,6 +1335,7 @@ class LineMatcher(object): The matches and non-matches are also printed on stdout. """ + __tracebackhide__ = True self._match_lines(lines2, lambda name, pat: re.match(pat, name), "re.match") def _match_lines(self, lines2, match_func, match_nickname): From 52ff1eaf37603c69b8381d6aead771325968cfb7 Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Wed, 10 Oct 2018 15:32:19 +0200 Subject: [PATCH 047/105] _getconftestmodules: optimize --- src/_pytest/config/__init__.py | 38 ++++++++++++++++++---------------- 1 file changed, 20 insertions(+), 18 deletions(-) diff --git a/src/_pytest/config/__init__.py b/src/_pytest/config/__init__.py index 0f8d2d5f3..efc5e1235 100644 --- a/src/_pytest/config/__init__.py +++ b/src/_pytest/config/__init__.py @@ -378,25 +378,27 @@ class PytestPluginManager(PluginManager): def _getconftestmodules(self, path): if self._noconftest: return [] - try: - return self._path2confmods[path] - except KeyError: - if path.isfile(): - clist = self._getconftestmodules(path.dirpath()) - else: - # XXX these days we may rather want to use config.rootdir - # and allow users to opt into looking into the rootdir parent - # directories instead of requiring to specify confcutdir - clist = [] - for parent in path.parts(): - if self._confcutdir and self._confcutdir.relto(parent): - continue - conftestpath = parent.join("conftest.py") - if conftestpath.isfile(): - mod = self._importconftest(conftestpath) - clist.append(mod) - self._path2confmods[path] = clist + if path.isfile(): + directory = path.dirpath() + else: + directory = path + try: + return self._path2confmods[directory] + except KeyError: + # XXX these days we may rather want to use config.rootdir + # and allow users to opt into looking into the rootdir parent + # directories instead of requiring to specify confcutdir + clist = [] + for parent in directory.parts(): + if self._confcutdir and self._confcutdir.relto(parent): + continue + conftestpath = parent.join("conftest.py") + if conftestpath.isfile(): + mod = self._importconftest(conftestpath) + clist.append(mod) + + self._path2confmods[directory] = clist return clist def _rget_with_confmod(self, name, path): From 67f40e18a7420f2112bbf10a8eca4f317517cab9 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Wed, 10 Oct 2018 18:23:11 -0300 Subject: [PATCH 048/105] Use attr.s(repr=False) because we customize MarkInfo's repr --- src/_pytest/mark/structures.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/_pytest/mark/structures.py b/src/_pytest/mark/structures.py index 2c4635bf4..ef2757d62 100644 --- a/src/_pytest/mark/structures.py +++ b/src/_pytest/mark/structures.py @@ -315,7 +315,7 @@ def _marked(func, mark): return any(mark == info.combined for info in func_mark) -@attr.s +@attr.s(repr=False) class MarkInfo(object): """ Marking object created by :class:`MarkDecorator` instances. """ From 5436e4299075236023e8875c563a7c74cad9bb2a Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Wed, 3 Oct 2018 20:07:59 -0300 Subject: [PATCH 049/105] Use pytest.fail(..., pytrace=False) when treating user errors This prevents an enormous and often useless stack trace from showing to end users. Fix #3867 Fix #2293 --- changelog/2293.feature.rst | 4 ++ changelog/2293.trivial.rst | 1 + src/_pytest/fixtures.py | 15 ++++--- src/_pytest/mark/__init__.py | 5 --- src/_pytest/mark/structures.py | 3 +- src/_pytest/nodes.py | 4 ++ src/_pytest/python.py | 50 +++++++++++---------- testing/python/fixture.py | 81 ++++++++++++++++------------------ testing/python/metafunc.py | 38 ++++++++-------- testing/test_mark.py | 4 +- testing/test_runner.py | 18 +++++++- 11 files changed, 124 insertions(+), 99 deletions(-) create mode 100644 changelog/2293.feature.rst create mode 100644 changelog/2293.trivial.rst diff --git a/changelog/2293.feature.rst b/changelog/2293.feature.rst new file mode 100644 index 000000000..5e56ba321 --- /dev/null +++ b/changelog/2293.feature.rst @@ -0,0 +1,4 @@ +Improve usage errors messages by hiding internal details which can be distracting and noisy. + +This has the side effect that some error conditions that previously raised generic errors (such as +``ValueError`` for unregistered marks) are now raising ``Failed`` exceptions. diff --git a/changelog/2293.trivial.rst b/changelog/2293.trivial.rst new file mode 100644 index 000000000..a11245127 --- /dev/null +++ b/changelog/2293.trivial.rst @@ -0,0 +1 @@ +The internal ``MarkerError`` exception has been removed. diff --git a/src/_pytest/fixtures.py b/src/_pytest/fixtures.py index 0b8b6b044..3dec56a35 100644 --- a/src/_pytest/fixtures.py +++ b/src/_pytest/fixtures.py @@ -579,7 +579,7 @@ class FixtureRequest(FuncargnamesCompatAttr): nodeid=funcitem.nodeid, typename=type(funcitem).__name__, ) - fail(msg) + fail(msg, pytrace=False) if has_params: frame = inspect.stack()[3] frameinfo = inspect.getframeinfo(frame[0]) @@ -600,7 +600,7 @@ class FixtureRequest(FuncargnamesCompatAttr): source_lineno, ) ) - fail(msg) + fail(msg, pytrace=False) else: # indices might not be set if old-style metafunc.addcall() was used param_index = funcitem.callspec.indices.get(argname, 0) @@ -718,10 +718,11 @@ def scope2index(scope, descr, where=None): try: return scopes.index(scope) except ValueError: - raise ValueError( - "{} {}has an unsupported scope value '{}'".format( + fail( + "{} {}got an unexpected scope value '{}'".format( descr, "from {} ".format(where) if where else "", scope - ) + ), + pytrace=False, ) @@ -854,7 +855,9 @@ class FixtureDef(object): self.argname = argname self.scope = scope self.scopenum = scope2index( - scope or "function", descr="fixture {}".format(func.__name__), where=baseid + scope or "function", + descr="Fixture '{}'".format(func.__name__), + where=baseid, ) self.params = params self.argnames = getfuncargnames(func, is_method=unittest) diff --git a/src/_pytest/mark/__init__.py b/src/_pytest/mark/__init__.py index eb25cd4aa..390057428 100644 --- a/src/_pytest/mark/__init__.py +++ b/src/_pytest/mark/__init__.py @@ -24,11 +24,6 @@ __all__ = [ ] -class MarkerError(Exception): - - """Error in use of a pytest marker/attribute.""" - - def param(*values, **kw): """Specify a parameter in `pytest.mark.parametrize`_ calls or :ref:`parametrized fixtures `. diff --git a/src/_pytest/mark/structures.py b/src/_pytest/mark/structures.py index ef2757d62..32822c2bb 100644 --- a/src/_pytest/mark/structures.py +++ b/src/_pytest/mark/structures.py @@ -6,6 +6,7 @@ from operator import attrgetter import attr +from _pytest.outcomes import fail from ..deprecated import MARK_PARAMETERSET_UNPACKING, MARK_INFO_ATTRIBUTE from ..compat import NOTSET, getfslineno, MappingMixin from six.moves import map @@ -393,7 +394,7 @@ class MarkGenerator(object): x = marker.split("(", 1)[0] values.add(x) if name not in self._markers: - raise AttributeError("%r not a registered marker" % (name,)) + fail("{!r} not a registered marker".format(name), pytrace=False) MARK_GEN = MarkGenerator() diff --git a/src/_pytest/nodes.py b/src/_pytest/nodes.py index 9cd758941..d80895ab5 100644 --- a/src/_pytest/nodes.py +++ b/src/_pytest/nodes.py @@ -9,6 +9,7 @@ import attr import _pytest import _pytest._code from _pytest.compat import getfslineno +from _pytest.outcomes import fail from _pytest.mark.structures import NodeKeywords, MarkInfo @@ -346,6 +347,9 @@ class Node(object): pass def _repr_failure_py(self, excinfo, style=None): + if excinfo.errisinstance(fail.Exception): + if not excinfo.value.pytrace: + return six.text_type(excinfo.value) fm = self.session._fixturemanager if excinfo.errisinstance(fm.FixtureLookupError): return excinfo.value.formatrepr() diff --git a/src/_pytest/python.py b/src/_pytest/python.py index 258901380..e9d05666f 100644 --- a/src/_pytest/python.py +++ b/src/_pytest/python.py @@ -13,7 +13,6 @@ from textwrap import dedent import py import six from _pytest.main import FSHookProxy -from _pytest.mark import MarkerError from _pytest.config import hookimpl import _pytest @@ -159,8 +158,8 @@ def pytest_generate_tests(metafunc): alt_spellings = ["parameterize", "parametrise", "parameterise"] for attr in alt_spellings: if hasattr(metafunc.function, attr): - msg = "{0} has '{1}', spelling should be 'parametrize'" - raise MarkerError(msg.format(metafunc.function.__name__, attr)) + msg = "{0} has '{1}' mark, spelling should be 'parametrize'" + fail(msg.format(metafunc.function.__name__, attr), pytrace=False) for marker in metafunc.definition.iter_markers(name="parametrize"): metafunc.parametrize(*marker.args, **marker.kwargs) @@ -760,12 +759,6 @@ class FunctionMixin(PyobjMixin): for entry in excinfo.traceback[1:-1]: entry.set_repr_style("short") - def _repr_failure_py(self, excinfo, style="long"): - if excinfo.errisinstance(fail.Exception): - if not excinfo.value.pytrace: - return six.text_type(excinfo.value) - return super(FunctionMixin, self)._repr_failure_py(excinfo, style=style) - def repr_failure(self, excinfo, outerr=None): assert outerr is None, "XXX outerr usage is deprecated" style = self.config.option.tbstyle @@ -987,7 +980,9 @@ class Metafunc(fixtures.FuncargnamesCompatAttr): ids = self._resolve_arg_ids(argnames, ids, parameters, item=self.definition) - scopenum = scope2index(scope, descr="call to {}".format(self.parametrize)) + scopenum = scope2index( + scope, descr="parametrize() call in {}".format(self.function.__name__) + ) # create the new calls: if we are parametrize() multiple times (by applying the decorator # more than once) then we accumulate those calls generating the cartesian product @@ -1026,15 +1021,16 @@ class Metafunc(fixtures.FuncargnamesCompatAttr): idfn = ids ids = None if ids: + func_name = self.function.__name__ if len(ids) != len(parameters): - raise ValueError( - "%d tests specified with %d ids" % (len(parameters), len(ids)) - ) + msg = "In {}: {} parameter sets specified, with different number of ids: {}" + fail(msg.format(func_name, len(parameters), len(ids)), pytrace=False) for id_value in ids: if id_value is not None and not isinstance(id_value, six.string_types): - msg = "ids must be list of strings, found: %s (type: %s)" - raise ValueError( - msg % (saferepr(id_value), type(id_value).__name__) + msg = "In {}: ids must be list of strings, found: {} (type: {!r})" + fail( + msg.format(func_name, saferepr(id_value), type(id_value)), + pytrace=False, ) ids = idmaker(argnames, parameters, idfn, ids, self.config, item=item) return ids @@ -1059,9 +1055,11 @@ class Metafunc(fixtures.FuncargnamesCompatAttr): valtypes = dict.fromkeys(argnames, "funcargs") for arg in indirect: if arg not in argnames: - raise ValueError( - "indirect given to %r: fixture %r doesn't exist" - % (self.function, arg) + fail( + "In {}: indirect fixture '{}' doesn't exist".format( + self.function.__name__, arg + ), + pytrace=False, ) valtypes[arg] = "params" return valtypes @@ -1075,19 +1073,25 @@ class Metafunc(fixtures.FuncargnamesCompatAttr): :raise ValueError: if validation fails. """ default_arg_names = set(get_default_arg_names(self.function)) + func_name = self.function.__name__ for arg in argnames: if arg not in self.fixturenames: if arg in default_arg_names: - raise ValueError( - "%r already takes an argument %r with a default value" - % (self.function, arg) + fail( + "In {}: function already takes an argument '{}' with a default value".format( + func_name, arg + ), + pytrace=False, ) else: if isinstance(indirect, (tuple, list)): name = "fixture" if arg in indirect else "argument" else: name = "fixture" if indirect else "argument" - raise ValueError("%r uses no %s %r" % (self.function, name, arg)) + fail( + "In {}: function uses no {} '{}'".format(func_name, name, arg), + pytrace=False, + ) def addcall(self, funcargs=None, id=NOTSET, param=NOTSET): """ Add a new call to the underlying test function during the collection phase of a test run. diff --git a/testing/python/fixture.py b/testing/python/fixture.py index a9e33b455..f10270326 100644 --- a/testing/python/fixture.py +++ b/testing/python/fixture.py @@ -1217,8 +1217,7 @@ class TestFixtureUsages(object): result = testdir.runpytest_inprocess() result.stdout.fnmatch_lines( ( - "*ValueError: fixture badscope from test_invalid_scope.py has an unsupported" - " scope value 'functions'" + "*Fixture 'badscope' from test_invalid_scope.py got an unexpected scope value 'functions'" ) ) @@ -3607,16 +3606,15 @@ class TestParameterizedSubRequest(object): ) result = testdir.runpytest() result.stdout.fnmatch_lines( - """ - E*Failed: The requested fixture has no parameter defined for test: - E* test_call_from_fixture.py::test_foo - E* - E*Requested fixture 'fix_with_param' defined in: - E*test_call_from_fixture.py:4 - E*Requested here: - E*test_call_from_fixture.py:9 - *1 error* - """ + [ + "The requested fixture has no parameter defined for test:", + " test_call_from_fixture.py::test_foo", + "Requested fixture 'fix_with_param' defined in:", + "test_call_from_fixture.py:4", + "Requested here:", + "test_call_from_fixture.py:9", + "*1 error in*", + ] ) def test_call_from_test(self, testdir): @@ -3634,16 +3632,15 @@ class TestParameterizedSubRequest(object): ) result = testdir.runpytest() result.stdout.fnmatch_lines( - """ - E*Failed: The requested fixture has no parameter defined for test: - E* test_call_from_test.py::test_foo - E* - E*Requested fixture 'fix_with_param' defined in: - E*test_call_from_test.py:4 - E*Requested here: - E*test_call_from_test.py:8 - *1 failed* - """ + [ + "The requested fixture has no parameter defined for test:", + " test_call_from_test.py::test_foo", + "Requested fixture 'fix_with_param' defined in:", + "test_call_from_test.py:4", + "Requested here:", + "test_call_from_test.py:8", + "*1 failed*", + ] ) def test_external_fixture(self, testdir): @@ -3665,16 +3662,16 @@ class TestParameterizedSubRequest(object): ) result = testdir.runpytest() result.stdout.fnmatch_lines( - """ - E*Failed: The requested fixture has no parameter defined for test: - E* test_external_fixture.py::test_foo - E* - E*Requested fixture 'fix_with_param' defined in: - E*conftest.py:4 - E*Requested here: - E*test_external_fixture.py:2 - *1 failed* - """ + [ + "The requested fixture has no parameter defined for test:", + " test_external_fixture.py::test_foo", + "", + "Requested fixture 'fix_with_param' defined in:", + "conftest.py:4", + "Requested here:", + "test_external_fixture.py:2", + "*1 failed*", + ] ) def test_non_relative_path(self, testdir): @@ -3709,16 +3706,16 @@ class TestParameterizedSubRequest(object): testdir.syspathinsert(fixdir) result = testdir.runpytest() result.stdout.fnmatch_lines( - """ - E*Failed: The requested fixture has no parameter defined for test: - E* test_foos.py::test_foo - E* - E*Requested fixture 'fix_with_param' defined in: - E*fix.py:4 - E*Requested here: - E*test_foos.py:4 - *1 failed* - """ + [ + "The requested fixture has no parameter defined for test:", + " test_foos.py::test_foo", + "", + "Requested fixture 'fix_with_param' defined in:", + "*fix.py:4", + "Requested here:", + "test_foos.py:4", + "*1 failed*", + ] ) diff --git a/testing/python/metafunc.py b/testing/python/metafunc.py index 2172c5e0c..fea59ee98 100644 --- a/testing/python/metafunc.py +++ b/testing/python/metafunc.py @@ -127,10 +127,11 @@ class TestMetafunc(object): pass metafunc = self.Metafunc(func) - try: + with pytest.raises( + pytest.fail.Exception, + match=r"parametrize\(\) call in func got an unexpected scope value 'doggy'", + ): metafunc.parametrize("x", [1], scope="doggy") - except ValueError as ve: - assert "has an unsupported scope value 'doggy'" in str(ve) def test_find_parametrized_scope(self): """unittest for _find_parametrized_scope (#3941)""" @@ -206,16 +207,13 @@ class TestMetafunc(object): metafunc = self.Metafunc(func) - pytest.raises( - ValueError, lambda: metafunc.parametrize("x", [1, 2], ids=["basic"]) - ) + with pytest.raises(pytest.fail.Exception): + metafunc.parametrize("x", [1, 2], ids=["basic"]) - pytest.raises( - ValueError, - lambda: metafunc.parametrize( + with pytest.raises(pytest.fail.Exception): + metafunc.parametrize( ("x", "y"), [("abc", "def"), ("ghi", "jkl")], ids=["one"] - ), - ) + ) @pytest.mark.issue510 def test_parametrize_empty_list(self): @@ -573,7 +571,7 @@ class TestMetafunc(object): pass metafunc = self.Metafunc(func) - with pytest.raises(ValueError): + with pytest.raises(pytest.fail.Exception): metafunc.parametrize("x, y", [("a", "b")], indirect=["x", "z"]) @pytest.mark.issue714 @@ -1189,7 +1187,9 @@ class TestMetafuncFunctional(object): ) result = testdir.runpytest() result.stdout.fnmatch_lines( - ["*ids must be list of strings, found: 2 (type: int)*"] + [ + "*In test_ids_numbers: ids must be list of strings, found: 2 (type: *'int'>)*" + ] ) def test_parametrize_with_identical_ids_get_unique_names(self, testdir): @@ -1326,13 +1326,13 @@ class TestMetafuncFunctional(object): attr ) ) - reprec = testdir.inline_run("--collectonly") - failures = reprec.getfailures() - assert len(failures) == 1 - expectederror = "MarkerError: test_foo has '{}', spelling should be 'parametrize'".format( - attr + result = testdir.runpytest("--collectonly") + result.stdout.fnmatch_lines( + [ + "test_foo has '{}' mark, spelling should be 'parametrize'".format(attr), + "*1 error in*", + ] ) - assert expectederror in failures[0].longrepr.reprcrash.message class TestMetafuncFunctionalAuto(object): diff --git a/testing/test_mark.py b/testing/test_mark.py index 3e5c86624..48a680297 100644 --- a/testing/test_mark.py +++ b/testing/test_mark.py @@ -247,7 +247,7 @@ def test_marker_without_description(testdir): ) ftdir = testdir.mkdir("ft1_dummy") testdir.tmpdir.join("conftest.py").move(ftdir.join("conftest.py")) - rec = testdir.runpytest_subprocess("--strict") + rec = testdir.runpytest("--strict") rec.assert_outcomes() @@ -302,7 +302,7 @@ def test_strict_prohibits_unregistered_markers(testdir): ) result = testdir.runpytest("--strict") assert result.ret != 0 - result.stdout.fnmatch_lines(["*unregisteredmark*not*registered*"]) + result.stdout.fnmatch_lines(["'unregisteredmark' not a registered marker"]) @pytest.mark.parametrize( diff --git a/testing/test_runner.py b/testing/test_runner.py index 741180692..b9538cfad 100644 --- a/testing/test_runner.py +++ b/testing/test_runner.py @@ -570,7 +570,8 @@ def test_pytest_exit_msg(testdir): result.stderr.fnmatch_lines(["Exit: oh noes"]) -def test_pytest_fail_notrace(testdir): +def test_pytest_fail_notrace_runtest(testdir): + """Test pytest.fail(..., pytrace=False) does not show tracebacks during test run.""" testdir.makepyfile( """ import pytest @@ -585,6 +586,21 @@ def test_pytest_fail_notrace(testdir): assert "def teardown_function" not in result.stdout.str() +def test_pytest_fail_notrace_collection(testdir): + """Test pytest.fail(..., pytrace=False) does not show tracebacks during collection.""" + testdir.makepyfile( + """ + import pytest + def some_internal_function(): + pytest.fail("hello", pytrace=False) + some_internal_function() + """ + ) + result = testdir.runpytest() + result.stdout.fnmatch_lines(["hello"]) + assert "def some_internal_function()" not in result.stdout.str() + + @pytest.mark.parametrize("str_prefix", ["u", ""]) def test_pytest_fail_notrace_non_ascii(testdir, str_prefix): """Fix pytest.fail with pytrace=False with non-ascii characters (#1178). From bf265a424d1d146978d3847142d543b46cc0f117 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Wed, 10 Oct 2018 19:35:49 -0300 Subject: [PATCH 050/105] Minor adjustments found during code review --- changelog/4012.bugfix.rst | 1 - changelog/4102.bugfix.rst | 1 + changelog/4102.feature.rst | 3 +++ src/_pytest/recwarn.py | 8 ++++---- 4 files changed, 8 insertions(+), 5 deletions(-) delete mode 100644 changelog/4012.bugfix.rst create mode 100644 changelog/4102.bugfix.rst diff --git a/changelog/4012.bugfix.rst b/changelog/4012.bugfix.rst deleted file mode 100644 index 11d8deef7..000000000 --- a/changelog/4012.bugfix.rst +++ /dev/null @@ -1 +0,0 @@ -``pytest.warn`` will capture previously-warned warnings in python2. Previously they were never raised. diff --git a/changelog/4102.bugfix.rst b/changelog/4102.bugfix.rst new file mode 100644 index 000000000..dd066c38d --- /dev/null +++ b/changelog/4102.bugfix.rst @@ -0,0 +1 @@ +``pytest.warn`` will capture previously-warned warnings in Python 2. Previously they were never raised. diff --git a/changelog/4102.feature.rst b/changelog/4102.feature.rst index b7916c841..ee43ddc24 100644 --- a/changelog/4102.feature.rst +++ b/changelog/4102.feature.rst @@ -1 +1,4 @@ Reimplement ``pytest.deprecated_call`` using ``pytest.warns`` so it supports the ``match='...'`` keyword argument. + +This has the side effect that ``pytest.deprecated_call`` now raises ``pytest.fail.Exception`` instead +of ``AssertionError``. diff --git a/src/_pytest/recwarn.py b/src/_pytest/recwarn.py index 592abdf5b..6ca1b0384 100644 --- a/src/_pytest/recwarn.py +++ b/src/_pytest/recwarn.py @@ -156,18 +156,18 @@ class WarningsRecorder(warnings.catch_warnings): if six.PY2: def warn(*args, **kwargs): - return self._warn(*args, **kwargs) + return self._saved_warn(*args, **kwargs) - warnings.warn, self._warn = warn, warnings.warn + warnings.warn, self._saved_warn = warn, warnings.warn return self def __exit__(self, *exc_info): if not self._entered: __tracebackhide__ = True raise RuntimeError("Cannot exit %r without entering first" % self) - # see above where `self.mp` is assigned + # see above where `self._saved_warn` is assigned if six.PY2: - warnings.warn = self._warn + warnings.warn = self._saved_warn super(WarningsRecorder, self).__exit__(*exc_info) From b48e23d54c7133e2612d6b1974dd26d994be10b9 Mon Sep 17 00:00:00 2001 From: Ronny Pfannschmidt Date: Sat, 15 Sep 2018 22:30:44 +0200 Subject: [PATCH 051/105] port interals of tmpdir to a basic pathlib implementation this is still lacking locking and cleanup of the folders --- .pre-commit-config.yaml | 2 + src/_pytest/main.py | 5 +- src/_pytest/tmpdir.py | 145 ++++++++++++++++++++++++++++------------ testing/test_tmpdir.py | 4 +- 4 files changed, 109 insertions(+), 47 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 7f48cd5d7..fc6d9e10c 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -20,7 +20,9 @@ repos: - id: check-yaml - id: debug-statements exclude: _pytest/debugging.py + language_version: python3 - id: flake8 + language_version: python3 - repo: https://github.com/asottile/pyupgrade rev: v1.8.0 hooks: diff --git a/src/_pytest/main.py b/src/_pytest/main.py index ce07285a4..bf4faaf6a 100644 --- a/src/_pytest/main.py +++ b/src/_pytest/main.py @@ -156,7 +156,10 @@ def pytest_addoption(parser): dest="basetemp", default=None, metavar="dir", - help="base temporary directory for this test run.", + help=( + "base temporary directory for this test run." + "(warning: this directory is removed if it exists)" + ), ) diff --git a/src/_pytest/tmpdir.py b/src/_pytest/tmpdir.py index 260d28422..f1d52d497 100644 --- a/src/_pytest/tmpdir.py +++ b/src/_pytest/tmpdir.py @@ -6,17 +6,110 @@ import re import pytest import py from _pytest.monkeypatch import MonkeyPatch +from .compat import Path +import attr +import shutil +import tempfile +def make_numbered_dir(root, prefix): + l_prefix = prefix.lower() + + def parse_num(p, cut=len(l_prefix)): + maybe_num = p.name[cut:] + try: + return int(maybe_num) + except ValueError: + return -1 + + for i in range(10): + # try up to 10 times to create the folder + max_existing = max( + ( + parse_num(x) + for x in root.iterdir() + if x.name.lower().startswith(l_prefix) + ), + default=-1, + ) + new_number = max_existing + 1 + new_path = root.joinpath("{}{}".format(prefix, new_number)) + try: + new_path.mkdir() + except Exception: + pass + else: + return new_path + + +def make_numbered_dir_with_cleanup(root, prefix, keep, consider_lock_dead_after): + p = make_numbered_dir(root, prefix) + # todo cleanup + return p + + +@attr.s +class TempPathFactory(object): + """docstring for ClassName""" + + given_basetemp = attr.ib() + trace = attr.ib() + _basetemp = attr.ib(default=None) + + @classmethod + def from_config(cls, config): + return cls( + given_basetemp=config.option.basetemp, trace=config.trace.get("tmpdir") + ) + + def mktemp(self, basename, numbered=True): + if not numbered: + p = self.getbasetemp().joinpath(basename) + p.mkdir() + else: + p = make_numbered_dir(root=self.getbasetemp(), prefix=basename) + self.trace("mktemp", p) + return p + + def getbasetemp(self): + """ return base temporary directory. """ + if self._basetemp is None: + if self.given_basetemp: + basetemp = Path(self.given_basetemp) + if basetemp.exists(): + shutil.rmtree(str(basetemp)) + basetemp.mkdir() + else: + temproot = Path(tempfile.gettempdir()) + user = get_user() + if user: + # use a sub-directory in the temproot to speed-up + # make_numbered_dir() call + rootdir = temproot.joinpath("pytest-of-{}".format(user)) + else: + rootdir = temproot + rootdir.mkdir(exist_ok=True) + basetemp = make_numbered_dir_with_cleanup( + prefix="pytest-", + root=rootdir, + keep=None, + consider_lock_dead_after=None, + ) + self._basetemp = t = basetemp + self.trace("new basetemp", t) + return t + else: + return self._basetemp + + +@attr.s class TempdirFactory(object): """Factory for temporary directories under the common base temp directory. The base directory can be configured using the ``--basetemp`` option. """ - def __init__(self, config): - self.config = config - self.trace = config.trace.get("tmpdir") + tmppath_factory = attr.ib() def ensuretemp(self, string, dir=1): """ (deprecated) return temporary directory path with @@ -33,46 +126,13 @@ class TempdirFactory(object): If ``numbered``, ensure the directory is unique by adding a number prefix greater than any existing one. """ - basetemp = self.getbasetemp() - if not numbered: - p = basetemp.mkdir(basename) - else: - p = py.path.local.make_numbered_dir( - prefix=basename, keep=0, rootdir=basetemp, lock_timeout=None - ) - self.trace("mktemp", p) - return p + return py.path.local(self.tmppath_factory.mktemp(basename, numbered).resolve()) def getbasetemp(self): - """ return base temporary directory. """ - try: - return self._basetemp - except AttributeError: - basetemp = self.config.option.basetemp - if basetemp: - basetemp = py.path.local(basetemp) - if basetemp.check(): - basetemp.remove() - basetemp.mkdir() - else: - temproot = py.path.local.get_temproot() - user = get_user() - if user: - # use a sub-directory in the temproot to speed-up - # make_numbered_dir() call - rootdir = temproot.join("pytest-of-%s" % user) - else: - rootdir = temproot - rootdir.ensure(dir=1) - basetemp = py.path.local.make_numbered_dir( - prefix="pytest-", rootdir=rootdir - ) - self._basetemp = t = basetemp.realpath() - self.trace("new basetemp", t) - return t + return py.path.local(self.tmppath_factory.getbasetemp().resolve()) def finish(self): - self.trace("finish") + self.tmppath_factory.trace("finish") def get_user(): @@ -87,10 +147,6 @@ def get_user(): return None -# backward compatibility -TempdirHandler = TempdirFactory - - def pytest_configure(config): """Create a TempdirFactory and attach it to the config object. @@ -99,7 +155,8 @@ def pytest_configure(config): to the tmpdir_factory session fixture. """ mp = MonkeyPatch() - t = TempdirFactory(config) + tmppath_handler = TempPathFactory.from_config(config) + t = TempdirFactory(tmppath_handler) config._cleanup.extend([mp.undo, t.finish]) mp.setattr(config, "_tmpdirhandler", t, raising=False) mp.setattr(pytest, "ensuretemp", t.ensuretemp, raising=False) diff --git a/testing/test_tmpdir.py b/testing/test_tmpdir.py index 487f9b21e..2acfec5c0 100644 --- a/testing/test_tmpdir.py +++ b/testing/test_tmpdir.py @@ -19,11 +19,11 @@ def test_ensuretemp(recwarn): class TestTempdirHandler(object): def test_mktemp(self, testdir): - from _pytest.tmpdir import TempdirFactory + from _pytest.tmpdir import TempdirFactory, TempPathFactory config = testdir.parseconfig() config.option.basetemp = testdir.mkdir("hello") - t = TempdirFactory(config) + t = TempdirFactory(TempPathFactory.from_config(config)) tmp = t.mktemp("world") assert tmp.relto(t.getbasetemp()) == "world0" tmp = t.mktemp("this") From 2e39fd89d1f54d924c10094659254e7c66096e0a Mon Sep 17 00:00:00 2001 From: Ronny Pfannschmidt Date: Sun, 16 Sep 2018 14:34:50 +0200 Subject: [PATCH 052/105] add python27 support by using reduce instead of max --- src/_pytest/tmpdir.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/_pytest/tmpdir.py b/src/_pytest/tmpdir.py index f1d52d497..bf4e9e6b0 100644 --- a/src/_pytest/tmpdir.py +++ b/src/_pytest/tmpdir.py @@ -2,6 +2,7 @@ from __future__ import absolute_import, division, print_function import re +from six.moves import reduce import pytest import py @@ -24,13 +25,14 @@ def make_numbered_dir(root, prefix): for i in range(10): # try up to 10 times to create the folder - max_existing = max( + max_existing = reduce( + max, ( parse_num(x) for x in root.iterdir() if x.name.lower().startswith(l_prefix) ), - default=-1, + -1, ) new_number = max_existing + 1 new_path = root.joinpath("{}{}".format(prefix, new_number)) From d053cdfbbb5a9da7a726d0f564c1ff7139e5e70c Mon Sep 17 00:00:00 2001 From: Ronny Pfannschmidt Date: Tue, 18 Sep 2018 22:43:48 +0200 Subject: [PATCH 053/105] factor out max and iterate on locks and cleanups --- src/_pytest/tmpdir.py | 92 ++++++++++++++++++++++++++++++++++--------- 1 file changed, 74 insertions(+), 18 deletions(-) diff --git a/src/_pytest/tmpdir.py b/src/_pytest/tmpdir.py index bf4e9e6b0..45a5e2d1b 100644 --- a/src/_pytest/tmpdir.py +++ b/src/_pytest/tmpdir.py @@ -2,8 +2,13 @@ from __future__ import absolute_import, division, print_function import re -from six.moves import reduce +import os +import atexit +import six +from functools import reduce + +from six.moves import map import pytest import py from _pytest.monkeypatch import MonkeyPatch @@ -13,10 +18,21 @@ import shutil import tempfile -def make_numbered_dir(root, prefix): +def find_prefixed(root, prefix): l_prefix = prefix.lower() + for x in root.iterdir(): + if x.name.lower().startswith(l_prefix): + yield x - def parse_num(p, cut=len(l_prefix)): + +def _max(iterable, default): + # needed due to python2.7 lacking the default argument for max + return reduce(max, iterable, default) + + +def make_numbered_dir(root, prefix): + + def parse_num(p, cut=len(prefix)): maybe_num = p.name[cut:] try: return int(maybe_num) @@ -25,15 +41,7 @@ def make_numbered_dir(root, prefix): for i in range(10): # try up to 10 times to create the folder - max_existing = reduce( - max, - ( - parse_num(x) - for x in root.iterdir() - if x.name.lower().startswith(l_prefix) - ), - -1, - ) + max_existing = _max(map(parse_num, find_prefixed(root, prefix)), -1) new_number = max_existing + 1 new_path = root.joinpath("{}{}".format(prefix, new_number)) try: @@ -42,13 +50,60 @@ def make_numbered_dir(root, prefix): pass else: return new_path + else: + raise EnvironmentError( + "could not create numbered dir with prefix {prefix} in {root})".format( + prefix=prefix, root=root)) + + +def create_cleanup_lock(p): + lock_path = p.joinpath('.lock') + fd = os.open(str(lock_path), os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o644) + pid = os.getpid() + spid = str(pid) + if not isinstance(spid, six.binary_type): + spid = spid.encode("ascii") + os.write(fd, spid) + os.close(fd) + if not lock_path.is_file(): + raise EnvironmentError("lock path got renamed after sucessfull creation") + return lock_path + + +def register_cleanup_lock_removal(lock_path): + pid = os.getpid() + + def cleanup_on_exit(lock_path=lock_path, original_pid=pid): + current_pid = os.getpid() + if current_pid != original_pid: + # fork + return + try: + lock_path.unlink() + except (OSError, IOError): + pass + return atexit.register(cleanup_on_exit) + + +def cleanup_numbered_dir(root, prefix, keep): + # todo + pass def make_numbered_dir_with_cleanup(root, prefix, keep, consider_lock_dead_after): - p = make_numbered_dir(root, prefix) - # todo cleanup - return p + for i in range(10): + try: + p = make_numbered_dir(root, prefix) + lock_path = create_cleanup_lock(p) + register_cleanup_lock_removal(lock_path) + except Exception as e: + raise + else: + cleanup_numbered_dir(root=root, prefix=prefix, keep=keep) + return p + else: + raise e @attr.s class TempPathFactory(object): @@ -76,7 +131,7 @@ class TempPathFactory(object): def getbasetemp(self): """ return base temporary directory. """ if self._basetemp is None: - if self.given_basetemp: + if self.given_basetemp is not None: basetemp = Path(self.given_basetemp) if basetemp.exists(): shutil.rmtree(str(basetemp)) @@ -94,9 +149,10 @@ class TempPathFactory(object): basetemp = make_numbered_dir_with_cleanup( prefix="pytest-", root=rootdir, - keep=None, - consider_lock_dead_after=None, + keep=3, + consider_lock_dead_after=10000, ) + assert basetemp is not None self._basetemp = t = basetemp self.trace("new basetemp", t) return t From 8e00280fc1d50bc2fb74cc1ea128e938bb29a691 Mon Sep 17 00:00:00 2001 From: Ronny Pfannschmidt Date: Wed, 19 Sep 2018 09:07:02 +0200 Subject: [PATCH 054/105] fix linting --- src/_pytest/tmpdir.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/_pytest/tmpdir.py b/src/_pytest/tmpdir.py index 45a5e2d1b..eea2a3c38 100644 --- a/src/_pytest/tmpdir.py +++ b/src/_pytest/tmpdir.py @@ -31,7 +31,6 @@ def _max(iterable, default): def make_numbered_dir(root, prefix): - def parse_num(p, cut=len(prefix)): maybe_num = p.name[cut:] try: @@ -53,11 +52,13 @@ def make_numbered_dir(root, prefix): else: raise EnvironmentError( "could not create numbered dir with prefix {prefix} in {root})".format( - prefix=prefix, root=root)) + prefix=prefix, root=root + ) + ) def create_cleanup_lock(p): - lock_path = p.joinpath('.lock') + lock_path = p.joinpath(".lock") fd = os.open(str(lock_path), os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o644) pid = os.getpid() spid = str(pid) @@ -82,6 +83,7 @@ def register_cleanup_lock_removal(lock_path): lock_path.unlink() except (OSError, IOError): pass + return atexit.register(cleanup_on_exit) @@ -96,14 +98,12 @@ def make_numbered_dir_with_cleanup(root, prefix, keep, consider_lock_dead_after) p = make_numbered_dir(root, prefix) lock_path = create_cleanup_lock(p) register_cleanup_lock_removal(lock_path) - except Exception as e: + except Exception: raise else: cleanup_numbered_dir(root=root, prefix=prefix, keep=keep) return p - else: - raise e @attr.s class TempPathFactory(object): From 66a690928cc847c48774906e5ea60f578ddb216c Mon Sep 17 00:00:00 2001 From: Ronny Pfannschmidt Date: Wed, 19 Sep 2018 18:17:10 +0200 Subject: [PATCH 055/105] bring in purepath and fix an assertion --- src/pytest.py | 3 +++ testing/test_tmpdir.py | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/src/pytest.py b/src/pytest.py index e173fd3d4..9ed7536d1 100644 --- a/src/pytest.py +++ b/src/pytest.py @@ -26,6 +26,7 @@ from _pytest.warning_types import ( RemovedInPytest4Warning, PytestExperimentalApiWarning, ) +from _pytest.compat import Path, PurePath set_trace = __pytestPDB.set_trace @@ -67,6 +68,8 @@ __all__ = [ "warns", "xfail", "yield_fixture", + "Path", + "PurePath", ] if __name__ == "__main__": diff --git a/testing/test_tmpdir.py b/testing/test_tmpdir.py index 2acfec5c0..0ea47aef4 100644 --- a/testing/test_tmpdir.py +++ b/testing/test_tmpdir.py @@ -111,7 +111,7 @@ def test_tmpdir_factory(testdir): def session_dir(tmpdir_factory): return tmpdir_factory.mktemp('data', numbered=False) def test_some(session_dir): - session_dir.isdir() + assert session_dir.isdir() """ ) reprec = testdir.inline_run() From ab3637d486cf48f46aaaa3627c3314ec7099ab06 Mon Sep 17 00:00:00 2001 From: Ronny Pfannschmidt Date: Thu, 20 Sep 2018 16:10:33 +0200 Subject: [PATCH 056/105] implement cleanup for unlocked folders --- src/_pytest/tmpdir.py | 109 +++++++++++++++++++++++++++++++---------- testing/test_tmpdir.py | 57 +++++++++++++++++++++ 2 files changed, 139 insertions(+), 27 deletions(-) diff --git a/src/_pytest/tmpdir.py b/src/_pytest/tmpdir.py index eea2a3c38..276085fdc 100644 --- a/src/_pytest/tmpdir.py +++ b/src/_pytest/tmpdir.py @@ -3,11 +3,12 @@ from __future__ import absolute_import, division, print_function import re import os +import errno import atexit - +import operator import six from functools import reduce - +import uuid from six.moves import map import pytest import py @@ -16,6 +17,10 @@ from .compat import Path import attr import shutil import tempfile +import itertools + + +get_lock_path = operator.methodcaller("joinpath", ".lock") def find_prefixed(root, prefix): @@ -25,22 +30,32 @@ def find_prefixed(root, prefix): yield x +def extract_suffixees(iter, prefix): + p_len = len(prefix) + for p in iter: + yield p.name[p_len:] + + +def find_suffixes(root, prefix): + return extract_suffixees(find_prefixed(root, prefix), prefix) + + +def parse_num(maybe_num): + try: + return int(maybe_num) + except ValueError: + return -1 + + def _max(iterable, default): # needed due to python2.7 lacking the default argument for max return reduce(max, iterable, default) def make_numbered_dir(root, prefix): - def parse_num(p, cut=len(prefix)): - maybe_num = p.name[cut:] - try: - return int(maybe_num) - except ValueError: - return -1 - for i in range(10): # try up to 10 times to create the folder - max_existing = _max(map(parse_num, find_prefixed(root, prefix)), -1) + max_existing = _max(map(parse_num, find_suffixes(root, prefix)), -1) new_number = max_existing + 1 new_path = root.joinpath("{}{}".format(prefix, new_number)) try: @@ -58,20 +73,29 @@ def make_numbered_dir(root, prefix): def create_cleanup_lock(p): - lock_path = p.joinpath(".lock") - fd = os.open(str(lock_path), os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o644) - pid = os.getpid() - spid = str(pid) - if not isinstance(spid, six.binary_type): - spid = spid.encode("ascii") - os.write(fd, spid) - os.close(fd) - if not lock_path.is_file(): - raise EnvironmentError("lock path got renamed after sucessfull creation") - return lock_path + lock_path = get_lock_path(p) + try: + fd = os.open(str(lock_path), os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o644) + except OSError as e: + if e.errno == errno.EEXIST: + six.raise_from( + EnvironmentError("cannot create lockfile in {path}".format(path=p)), e + ) + else: + raise + else: + pid = os.getpid() + spid = str(pid) + if not isinstance(spid, six.binary_type): + spid = spid.encode("ascii") + os.write(fd, spid) + os.close(fd) + if not lock_path.is_file(): + raise EnvironmentError("lock path got renamed after sucessfull creation") + return lock_path -def register_cleanup_lock_removal(lock_path): +def register_cleanup_lock_removal(lock_path, register=atexit.register): pid = os.getpid() def cleanup_on_exit(lock_path=lock_path, original_pid=pid): @@ -84,12 +108,33 @@ def register_cleanup_lock_removal(lock_path): except (OSError, IOError): pass - return atexit.register(cleanup_on_exit) + return register(cleanup_on_exit) -def cleanup_numbered_dir(root, prefix, keep): - # todo - pass +def delete_a_numbered_dir(path): + create_cleanup_lock(path) + parent = path.parent + + garbage = parent.joinpath("garbage-{}".format(uuid.uuid4())) + path.rename(garbage) + shutil.rmtree(str(garbage)) + + +def is_deletable(path, consider_lock_dead_after): + lock = get_lock_path(path) + if not lock.exists(): + return True + + +def cleanup_numbered_dir(root, prefix, keep, consider_lock_dead_after): + max_existing = _max(map(parse_num, find_suffixes(root, prefix)), -1) + max_delete = max_existing - keep + paths = find_prefixed(root, prefix) + paths, paths2 = itertools.tee(paths) + numbers = map(parse_num, extract_suffixees(paths2, prefix)) + for path, number in zip(paths, numbers): + if number <= max_delete and is_deletable(path, consider_lock_dead_after): + delete_a_numbered_dir(path) def make_numbered_dir_with_cleanup(root, prefix, keep, consider_lock_dead_after): @@ -101,7 +146,12 @@ def make_numbered_dir_with_cleanup(root, prefix, keep, consider_lock_dead_after) except Exception: raise else: - cleanup_numbered_dir(root=root, prefix=prefix, keep=keep) + cleanup_numbered_dir( + root=root, + prefix=prefix, + keep=keep, + consider_lock_dead_after=consider_lock_dead_after, + ) return p @@ -244,3 +294,8 @@ def tmpdir(request, tmpdir_factory): name = name[:MAXVAL] x = tmpdir_factory.mktemp(name, numbered=True) return x + + +@pytest.fixture +def tmp_path(tmpdir): + return Path(tmpdir) diff --git a/testing/test_tmpdir.py b/testing/test_tmpdir.py index 0ea47aef4..5b460e628 100644 --- a/testing/test_tmpdir.py +++ b/testing/test_tmpdir.py @@ -184,3 +184,60 @@ def test_get_user(monkeypatch): monkeypatch.delenv("USER", raising=False) monkeypatch.delenv("USERNAME", raising=False) assert get_user() is None + + +class TestNumberedDir(object): + PREFIX = "fun-" + + def test_make(self, tmp_path): + from _pytest.tmpdir import make_numbered_dir + + for i in range(10): + d = make_numbered_dir(root=tmp_path, prefix=self.PREFIX) + assert d.name.startswith(self.PREFIX) + assert d.name.endswith(str(i)) + + def test_cleanup_lock_create(self, tmp_path): + d = tmp_path.joinpath("test") + d.mkdir() + from _pytest.tmpdir import create_cleanup_lock + + lockfile = create_cleanup_lock(d) + with pytest.raises(EnvironmentError, match="cannot create lockfile in .*"): + create_cleanup_lock(d) + + lockfile.unlink() + + def test_lock_register_cleanup_removal(self, tmp_path): + from _pytest.tmpdir import create_cleanup_lock, register_cleanup_lock_removal + + lock = create_cleanup_lock(tmp_path) + + registry = [] + register_cleanup_lock_removal(lock, register=registry.append) + + cleanup_func, = registry + + assert lock.is_file() + + cleanup_func(original_pid="intentionally_different") + + assert lock.is_file() + + cleanup_func() + + assert not lock.exists() + + cleanup_func() + + assert not lock.exists() + + def test_cleanup_keep(self, tmp_path): + self.test_make(tmp_path) + from _pytest.tmpdir import cleanup_numbered_dir + + cleanup_numbered_dir( + root=tmp_path, prefix=self.PREFIX, keep=2, consider_lock_dead_after=0 + ) + a, b = tmp_path.iterdir() + print(a, b) From 8b4a29357e24cab087202135a92ef603439c194d Mon Sep 17 00:00:00 2001 From: Ronny Pfannschmidt Date: Fri, 21 Sep 2018 15:28:49 +0200 Subject: [PATCH 057/105] fix typo --- src/_pytest/tmpdir.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/_pytest/tmpdir.py b/src/_pytest/tmpdir.py index 276085fdc..894a6b70e 100644 --- a/src/_pytest/tmpdir.py +++ b/src/_pytest/tmpdir.py @@ -30,14 +30,14 @@ def find_prefixed(root, prefix): yield x -def extract_suffixees(iter, prefix): +def extract_suffixes(iter, prefix): p_len = len(prefix) for p in iter: yield p.name[p_len:] def find_suffixes(root, prefix): - return extract_suffixees(find_prefixed(root, prefix), prefix) + return extract_suffixes(find_prefixed(root, prefix), prefix) def parse_num(maybe_num): @@ -111,7 +111,7 @@ def register_cleanup_lock_removal(lock_path, register=atexit.register): return register(cleanup_on_exit) -def delete_a_numbered_dir(path): +def delete_a_numbered_dir(path, consider_lock_dead_after): create_cleanup_lock(path) parent = path.parent @@ -131,10 +131,10 @@ def cleanup_numbered_dir(root, prefix, keep, consider_lock_dead_after): max_delete = max_existing - keep paths = find_prefixed(root, prefix) paths, paths2 = itertools.tee(paths) - numbers = map(parse_num, extract_suffixees(paths2, prefix)) + numbers = map(parse_num, extract_suffixes(paths2, prefix)) for path, number in zip(paths, numbers): if number <= max_delete and is_deletable(path, consider_lock_dead_after): - delete_a_numbered_dir(path) + delete_a_numbered_dir(path, consider_lock_dead_after) def make_numbered_dir_with_cleanup(root, prefix, keep, consider_lock_dead_after): From b3a5b0ebe1fa759cea7dcdd82d17020648234c61 Mon Sep 17 00:00:00 2001 From: Ronny Pfannschmidt Date: Wed, 26 Sep 2018 11:36:03 +0200 Subject: [PATCH 058/105] remove path from exposure --- src/pytest.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/pytest.py b/src/pytest.py index 9ed7536d1..e173fd3d4 100644 --- a/src/pytest.py +++ b/src/pytest.py @@ -26,7 +26,6 @@ from _pytest.warning_types import ( RemovedInPytest4Warning, PytestExperimentalApiWarning, ) -from _pytest.compat import Path, PurePath set_trace = __pytestPDB.set_trace @@ -68,8 +67,6 @@ __all__ = [ "warns", "xfail", "yield_fixture", - "Path", - "PurePath", ] if __name__ == "__main__": From 642cd86dd1723b4ed997a99f3e089eeefdf8af08 Mon Sep 17 00:00:00 2001 From: Ronny Pfannschmidt Date: Wed, 26 Sep 2018 20:41:33 +0200 Subject: [PATCH 059/105] shape up removal and lock destruction --- src/_pytest/tmpdir.py | 52 ++++++++++++++++++++++++++++-------------- testing/test_tmpdir.py | 12 ++++++++++ 2 files changed, 47 insertions(+), 17 deletions(-) diff --git a/src/_pytest/tmpdir.py b/src/_pytest/tmpdir.py index 894a6b70e..a843b55c5 100644 --- a/src/_pytest/tmpdir.py +++ b/src/_pytest/tmpdir.py @@ -111,7 +111,7 @@ def register_cleanup_lock_removal(lock_path, register=atexit.register): return register(cleanup_on_exit) -def delete_a_numbered_dir(path, consider_lock_dead_after): +def delete_a_numbered_dir(path): create_cleanup_lock(path) parent = path.parent @@ -120,24 +120,47 @@ def delete_a_numbered_dir(path, consider_lock_dead_after): shutil.rmtree(str(garbage)) -def is_deletable(path, consider_lock_dead_after): +def ensure_deletable(path, consider_lock_dead_after): lock = get_lock_path(path) if not lock.exists(): return True + try: + lock_time = lock.stat().st_mtime + except Exception: + return False + else: + if lock_time > consider_lock_dead_after: + lock.unlink() + return True + else: + return False -def cleanup_numbered_dir(root, prefix, keep, consider_lock_dead_after): +def try_cleanup(path, consider_lock_dead_after): + if ensure_deletable(path, consider_lock_dead_after): + delete_a_numbered_dir(path) + + +def cleanup_candidates(root, prefix, keep): max_existing = _max(map(parse_num, find_suffixes(root, prefix)), -1) max_delete = max_existing - keep paths = find_prefixed(root, prefix) paths, paths2 = itertools.tee(paths) numbers = map(parse_num, extract_suffixes(paths2, prefix)) for path, number in zip(paths, numbers): - if number <= max_delete and is_deletable(path, consider_lock_dead_after): - delete_a_numbered_dir(path, consider_lock_dead_after) + if number <= max_delete: + yield path -def make_numbered_dir_with_cleanup(root, prefix, keep, consider_lock_dead_after): +def cleanup_numbered_dir(root, prefix, keep, consider_lock_dead_after): + for path in cleanup_candidates(root, prefix, keep): + try_cleanup(path, consider_lock_dead_after) + known_garbage = list(root.glob("garbage-*")) + for path in known_garbage: + try_cleanup(path, consider_lock_dead_after) + + +def make_numbered_dir_with_cleanup(root, prefix, keep, lock_timeout): for i in range(10): try: p = make_numbered_dir(root, prefix) @@ -146,6 +169,7 @@ def make_numbered_dir_with_cleanup(root, prefix, keep, consider_lock_dead_after) except Exception: raise else: + consider_lock_dead_after = p.stat().st_mtime + lock_timeout cleanup_numbered_dir( root=root, prefix=prefix, @@ -188,19 +212,13 @@ class TempPathFactory(object): basetemp.mkdir() else: temproot = Path(tempfile.gettempdir()) - user = get_user() - if user: - # use a sub-directory in the temproot to speed-up - # make_numbered_dir() call - rootdir = temproot.joinpath("pytest-of-{}".format(user)) - else: - rootdir = temproot + user = get_user() or "unknown" + # use a sub-directory in the temproot to speed-up + # make_numbered_dir() call + rootdir = temproot.joinpath("pytest-of-{}".format(user)) rootdir.mkdir(exist_ok=True) basetemp = make_numbered_dir_with_cleanup( - prefix="pytest-", - root=rootdir, - keep=3, - consider_lock_dead_after=10000, + prefix="pytest-", root=rootdir, keep=3, lock_timeout=10000 ) assert basetemp is not None self._basetemp = t = basetemp diff --git a/testing/test_tmpdir.py b/testing/test_tmpdir.py index 5b460e628..360614673 100644 --- a/testing/test_tmpdir.py +++ b/testing/test_tmpdir.py @@ -143,6 +143,7 @@ def break_getuser(monkeypatch): monkeypatch.delenv(envvar, raising=False) +@pytest.mark.skip(reason="creates random tmpdirs as part of a system level test") @pytest.mark.usefixtures("break_getuser") @pytest.mark.skipif(sys.platform.startswith("win"), reason="no os.getuid on windows") def test_tmpdir_fallback_uid_not_found(testdir): @@ -161,6 +162,7 @@ def test_tmpdir_fallback_uid_not_found(testdir): reprec.assertoutcome(passed=1) +@pytest.mark.skip(reason="creates random tmpdirs as part of a system level test") @pytest.mark.usefixtures("break_getuser") @pytest.mark.skipif(sys.platform.startswith("win"), reason="no os.getuid on windows") def test_get_user_uid_not_found(): @@ -241,3 +243,13 @@ class TestNumberedDir(object): ) a, b = tmp_path.iterdir() print(a, b) + + def test_cleanup_locked(self, tmp_path): + + from _pytest import tmpdir + + p = tmpdir.make_numbered_dir(root=tmp_path, prefix=self.PREFIX) + + tmpdir.create_cleanup_lock(p) + assert not tmpdir.ensure_deletable(p, p.stat().st_mtime + 1) + assert tmpdir.ensure_deletable(p, p.stat().st_mtime - 1) From 2532dc1dbbdf49f2c32638d3caa19c4a7579a9b8 Mon Sep 17 00:00:00 2001 From: Ronny Pfannschmidt Date: Thu, 27 Sep 2018 11:23:21 +0200 Subject: [PATCH 060/105] fix up lock consideration argument --- src/_pytest/tmpdir.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/src/_pytest/tmpdir.py b/src/_pytest/tmpdir.py index a843b55c5..c6a079f4e 100644 --- a/src/_pytest/tmpdir.py +++ b/src/_pytest/tmpdir.py @@ -120,7 +120,7 @@ def delete_a_numbered_dir(path): shutil.rmtree(str(garbage)) -def ensure_deletable(path, consider_lock_dead_after): +def ensure_deletable(path, consider_lock_dead_if_created_before): lock = get_lock_path(path) if not lock.exists(): return True @@ -129,15 +129,15 @@ def ensure_deletable(path, consider_lock_dead_after): except Exception: return False else: - if lock_time > consider_lock_dead_after: + if lock_time < consider_lock_dead_if_created_before: lock.unlink() return True else: return False -def try_cleanup(path, consider_lock_dead_after): - if ensure_deletable(path, consider_lock_dead_after): +def try_cleanup(path, consider_lock_dead_if_created_before): + if ensure_deletable(path, consider_lock_dead_if_created_before): delete_a_numbered_dir(path) @@ -152,12 +152,12 @@ def cleanup_candidates(root, prefix, keep): yield path -def cleanup_numbered_dir(root, prefix, keep, consider_lock_dead_after): +def cleanup_numbered_dir(root, prefix, keep, consider_lock_dead_if_created_before): for path in cleanup_candidates(root, prefix, keep): - try_cleanup(path, consider_lock_dead_after) + try_cleanup(path, consider_lock_dead_if_created_before) known_garbage = list(root.glob("garbage-*")) for path in known_garbage: - try_cleanup(path, consider_lock_dead_after) + try_cleanup(path, consider_lock_dead_if_created_before) def make_numbered_dir_with_cleanup(root, prefix, keep, lock_timeout): @@ -169,12 +169,12 @@ def make_numbered_dir_with_cleanup(root, prefix, keep, lock_timeout): except Exception: raise else: - consider_lock_dead_after = p.stat().st_mtime + lock_timeout + consider_lock_dead_if_created_before = p.stat().st_mtime + lock_timeout cleanup_numbered_dir( root=root, prefix=prefix, keep=keep, - consider_lock_dead_after=consider_lock_dead_after, + consider_lock_dead_if_created_before=consider_lock_dead_if_created_before, ) return p From d76fa59b35edfffa91ce24e19c9f17f75f510546 Mon Sep 17 00:00:00 2001 From: Ronny Pfannschmidt Date: Fri, 28 Sep 2018 22:06:43 +0200 Subject: [PATCH 061/105] fix lock timeouts for good this time --- src/_pytest/tmpdir.py | 14 +++++++++----- testing/test_tmpdir.py | 14 +++++++++++--- 2 files changed, 20 insertions(+), 8 deletions(-) diff --git a/src/_pytest/tmpdir.py b/src/_pytest/tmpdir.py index c6a079f4e..b9bd12afc 100644 --- a/src/_pytest/tmpdir.py +++ b/src/_pytest/tmpdir.py @@ -20,6 +20,8 @@ import tempfile import itertools +LOCK_TIMEOUT = 60 * 60 * 3 + get_lock_path = operator.methodcaller("joinpath", ".lock") @@ -155,21 +157,21 @@ def cleanup_candidates(root, prefix, keep): def cleanup_numbered_dir(root, prefix, keep, consider_lock_dead_if_created_before): for path in cleanup_candidates(root, prefix, keep): try_cleanup(path, consider_lock_dead_if_created_before) - known_garbage = list(root.glob("garbage-*")) - for path in known_garbage: + for path in root.glob("garbage-*"): try_cleanup(path, consider_lock_dead_if_created_before) def make_numbered_dir_with_cleanup(root, prefix, keep, lock_timeout): + e = None for i in range(10): try: p = make_numbered_dir(root, prefix) lock_path = create_cleanup_lock(p) register_cleanup_lock_removal(lock_path) - except Exception: - raise + except Exception as e: + pass else: - consider_lock_dead_if_created_before = p.stat().st_mtime + lock_timeout + consider_lock_dead_if_created_before = p.stat().st_mtime - lock_timeout cleanup_numbered_dir( root=root, prefix=prefix, @@ -177,6 +179,8 @@ def make_numbered_dir_with_cleanup(root, prefix, keep, lock_timeout): consider_lock_dead_if_created_before=consider_lock_dead_if_created_before, ) return p + assert e is not None + raise e @attr.s diff --git a/testing/test_tmpdir.py b/testing/test_tmpdir.py index 360614673..02687f6fc 100644 --- a/testing/test_tmpdir.py +++ b/testing/test_tmpdir.py @@ -239,7 +239,10 @@ class TestNumberedDir(object): from _pytest.tmpdir import cleanup_numbered_dir cleanup_numbered_dir( - root=tmp_path, prefix=self.PREFIX, keep=2, consider_lock_dead_after=0 + root=tmp_path, + prefix=self.PREFIX, + keep=2, + consider_lock_dead_if_created_before=0, ) a, b = tmp_path.iterdir() print(a, b) @@ -251,5 +254,10 @@ class TestNumberedDir(object): p = tmpdir.make_numbered_dir(root=tmp_path, prefix=self.PREFIX) tmpdir.create_cleanup_lock(p) - assert not tmpdir.ensure_deletable(p, p.stat().st_mtime + 1) - assert tmpdir.ensure_deletable(p, p.stat().st_mtime - 1) + + assert not tmpdir.ensure_deletable( + p, consider_lock_dead_if_created_before=p.stat().st_mtime - 1 + ) + assert tmpdir.ensure_deletable( + p, consider_lock_dead_if_created_before=p.stat().st_mtime + 1 + ) From fed4f73a61384189f591294e8ca0dc9ad50ddb89 Mon Sep 17 00:00:00 2001 From: Ronny Pfannschmidt Date: Fri, 28 Sep 2018 23:09:00 +0200 Subject: [PATCH 062/105] ignore rmtree errors --- src/_pytest/tmpdir.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/_pytest/tmpdir.py b/src/_pytest/tmpdir.py index b9bd12afc..80856c0f1 100644 --- a/src/_pytest/tmpdir.py +++ b/src/_pytest/tmpdir.py @@ -119,7 +119,7 @@ def delete_a_numbered_dir(path): garbage = parent.joinpath("garbage-{}".format(uuid.uuid4())) path.rename(garbage) - shutil.rmtree(str(garbage)) + shutil.rmtree(str(garbage), ignore_errors=True) def ensure_deletable(path, consider_lock_dead_if_created_before): From 85cc9b8f128ae3cc5fd14b0b301b69ef8ea87128 Mon Sep 17 00:00:00 2001 From: Ronny Pfannschmidt Date: Sat, 29 Sep 2018 11:42:31 +0200 Subject: [PATCH 063/105] move all the things into _pytest.pathlib --- src/_pytest/assertion/rewrite.py | 3 +- src/_pytest/cacheprovider.py | 3 +- src/_pytest/compat.py | 7 -- src/_pytest/pathlib.py | 183 +++++++++++++++++++++++++++++++ src/_pytest/paths.py | 2 +- src/_pytest/pytester.py | 7 +- src/_pytest/tmpdir.py | 174 +---------------------------- testing/test_tmpdir.py | 18 +-- 8 files changed, 202 insertions(+), 195 deletions(-) create mode 100644 src/_pytest/pathlib.py diff --git a/src/_pytest/assertion/rewrite.py b/src/_pytest/assertion/rewrite.py index 7a11c4ec1..f61496050 100644 --- a/src/_pytest/assertion/rewrite.py +++ b/src/_pytest/assertion/rewrite.py @@ -17,7 +17,8 @@ import atomicwrites import py from _pytest.assertion import util -from _pytest.compat import PurePath, spec_from_file_location +from _pytest.pathlib import PurePath +from _pytest.compat import spec_from_file_location from _pytest.paths import fnmatch_ex # pytest caches rewritten pycs in __pycache__. diff --git a/src/_pytest/cacheprovider.py b/src/_pytest/cacheprovider.py index 87e24894b..be291bbff 100755 --- a/src/_pytest/cacheprovider.py +++ b/src/_pytest/cacheprovider.py @@ -16,7 +16,8 @@ import json import shutil from . import paths -from .compat import _PY2 as PY2, Path +from .compat import _PY2 as PY2 +from .pathlib import Path README_CONTENT = u"""\ # pytest cache directory # diff --git a/src/_pytest/compat.py b/src/_pytest/compat.py index 02cad24cc..3798f4eb1 100644 --- a/src/_pytest/compat.py +++ b/src/_pytest/compat.py @@ -23,8 +23,6 @@ except ImportError: # pragma: no cover # Only available in Python 3.4+ or as a backport enum = None -__all__ = ["Path", "PurePath"] - _PY3 = sys.version_info > (3, 0) _PY2 = not _PY3 @@ -41,11 +39,6 @@ PY35 = sys.version_info[:2] >= (3, 5) PY36 = sys.version_info[:2] >= (3, 6) MODULE_NOT_FOUND_ERROR = "ModuleNotFoundError" if PY36 else "ImportError" -if PY36: - from pathlib import Path, PurePath -else: - from pathlib2 import Path, PurePath - if _PY3: from collections.abc import MutableMapping as MappingMixin diff --git a/src/_pytest/pathlib.py b/src/_pytest/pathlib.py new file mode 100644 index 000000000..2d9f1d640 --- /dev/null +++ b/src/_pytest/pathlib.py @@ -0,0 +1,183 @@ + +import os +import errno +import atexit +import operator +import six +from functools import reduce +import uuid +from six.moves import map +import itertools +import shutil + +from .compat import PY36 + +if PY36: + from pathlib import Path, PurePath +else: + from pathlib2 import Path, PurePath + +__all__ = ["Path", "PurePath"] + + +LOCK_TIMEOUT = 60 * 60 * 3 + +get_lock_path = operator.methodcaller("joinpath", ".lock") + + +def find_prefixed(root, prefix): + l_prefix = prefix.lower() + for x in root.iterdir(): + if x.name.lower().startswith(l_prefix): + yield x + + +def extract_suffixes(iter, prefix): + p_len = len(prefix) + for p in iter: + yield p.name[p_len:] + + +def find_suffixes(root, prefix): + return extract_suffixes(find_prefixed(root, prefix), prefix) + + +def parse_num(maybe_num): + try: + return int(maybe_num) + except ValueError: + return -1 + + +def _max(iterable, default): + # needed due to python2.7 lacking the default argument for max + return reduce(max, iterable, default) + + +def make_numbered_dir(root, prefix): + for i in range(10): + # try up to 10 times to create the folder + max_existing = _max(map(parse_num, find_suffixes(root, prefix)), -1) + new_number = max_existing + 1 + new_path = root.joinpath("{}{}".format(prefix, new_number)) + try: + new_path.mkdir() + except Exception: + pass + else: + return new_path + else: + raise EnvironmentError( + "could not create numbered dir with prefix {prefix} in {root})".format( + prefix=prefix, root=root + ) + ) + + +def create_cleanup_lock(p): + lock_path = get_lock_path(p) + try: + fd = os.open(str(lock_path), os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o644) + except OSError as e: + if e.errno == errno.EEXIST: + six.raise_from( + EnvironmentError("cannot create lockfile in {path}".format(path=p)), e + ) + else: + raise + else: + pid = os.getpid() + spid = str(pid) + if not isinstance(spid, six.binary_type): + spid = spid.encode("ascii") + os.write(fd, spid) + os.close(fd) + if not lock_path.is_file(): + raise EnvironmentError("lock path got renamed after sucessfull creation") + return lock_path + + +def register_cleanup_lock_removal(lock_path, register=atexit.register): + pid = os.getpid() + + def cleanup_on_exit(lock_path=lock_path, original_pid=pid): + current_pid = os.getpid() + if current_pid != original_pid: + # fork + return + try: + lock_path.unlink() + except (OSError, IOError): + pass + + return register(cleanup_on_exit) + + +def delete_a_numbered_dir(path): + create_cleanup_lock(path) + parent = path.parent + + garbage = parent.joinpath("garbage-{}".format(uuid.uuid4())) + path.rename(garbage) + shutil.rmtree(str(garbage), ignore_errors=True) + + +def ensure_deletable(path, consider_lock_dead_if_created_before): + lock = get_lock_path(path) + if not lock.exists(): + return True + try: + lock_time = lock.stat().st_mtime + except Exception: + return False + else: + if lock_time < consider_lock_dead_if_created_before: + lock.unlink() + return True + else: + return False + + +def try_cleanup(path, consider_lock_dead_if_created_before): + if ensure_deletable(path, consider_lock_dead_if_created_before): + delete_a_numbered_dir(path) + + +def cleanup_candidates(root, prefix, keep): + max_existing = _max(map(parse_num, find_suffixes(root, prefix)), -1) + max_delete = max_existing - keep + paths = find_prefixed(root, prefix) + paths, paths2 = itertools.tee(paths) + numbers = map(parse_num, extract_suffixes(paths2, prefix)) + for path, number in zip(paths, numbers): + if number <= max_delete: + yield path + + +def cleanup_numbered_dir(root, prefix, keep, consider_lock_dead_if_created_before): + for path in cleanup_candidates(root, prefix, keep): + try_cleanup(path, consider_lock_dead_if_created_before) + for path in root.glob("garbage-*"): + try_cleanup(path, consider_lock_dead_if_created_before) + + +def make_numbered_dir_with_cleanup(root, prefix, keep, lock_timeout): + e = None + for i in range(10): + try: + p = make_numbered_dir(root, prefix) + lock_path = create_cleanup_lock(p) + register_cleanup_lock_removal(lock_path) + except Exception as e: + pass + else: + consider_lock_dead_if_created_before = p.stat().st_mtime - lock_timeout + cleanup_numbered_dir( + root=root, + prefix=prefix, + keep=keep, + consider_lock_dead_if_created_before=consider_lock_dead_if_created_before, + ) + return p + assert e is not None + raise e diff --git a/src/_pytest/paths.py b/src/_pytest/paths.py index 031ea6b26..3507cae7f 100644 --- a/src/_pytest/paths.py +++ b/src/_pytest/paths.py @@ -5,7 +5,7 @@ import sys import six -from .compat import Path, PurePath +from .pathlib import Path, PurePath def resolve_from_str(input, root): diff --git a/src/_pytest/pytester.py b/src/_pytest/pytester.py index 1e64c1747..85f824784 100644 --- a/src/_pytest/pytester.py +++ b/src/_pytest/pytester.py @@ -17,13 +17,14 @@ from weakref import WeakKeyDictionary from _pytest.capture import MultiCapture, SysCapture from _pytest._code import Source -import py -import pytest from _pytest.main import Session, EXIT_OK from _pytest.assertion.rewrite import AssertionRewritingHook -from _pytest.compat import Path +from _pytest.pathlib import Path from _pytest.compat import safe_str +import py +import pytest + IGNORE_PAM = [ # filenames added when obtaining details about the current user u"/var/lib/sss/mc/passwd" ] diff --git a/src/_pytest/tmpdir.py b/src/_pytest/tmpdir.py index 80856c0f1..40a9cbf90 100644 --- a/src/_pytest/tmpdir.py +++ b/src/_pytest/tmpdir.py @@ -2,185 +2,13 @@ from __future__ import absolute_import, division, print_function import re -import os -import errno -import atexit -import operator -import six -from functools import reduce -import uuid -from six.moves import map import pytest import py from _pytest.monkeypatch import MonkeyPatch -from .compat import Path import attr import shutil import tempfile -import itertools - - -LOCK_TIMEOUT = 60 * 60 * 3 - -get_lock_path = operator.methodcaller("joinpath", ".lock") - - -def find_prefixed(root, prefix): - l_prefix = prefix.lower() - for x in root.iterdir(): - if x.name.lower().startswith(l_prefix): - yield x - - -def extract_suffixes(iter, prefix): - p_len = len(prefix) - for p in iter: - yield p.name[p_len:] - - -def find_suffixes(root, prefix): - return extract_suffixes(find_prefixed(root, prefix), prefix) - - -def parse_num(maybe_num): - try: - return int(maybe_num) - except ValueError: - return -1 - - -def _max(iterable, default): - # needed due to python2.7 lacking the default argument for max - return reduce(max, iterable, default) - - -def make_numbered_dir(root, prefix): - for i in range(10): - # try up to 10 times to create the folder - max_existing = _max(map(parse_num, find_suffixes(root, prefix)), -1) - new_number = max_existing + 1 - new_path = root.joinpath("{}{}".format(prefix, new_number)) - try: - new_path.mkdir() - except Exception: - pass - else: - return new_path - else: - raise EnvironmentError( - "could not create numbered dir with prefix {prefix} in {root})".format( - prefix=prefix, root=root - ) - ) - - -def create_cleanup_lock(p): - lock_path = get_lock_path(p) - try: - fd = os.open(str(lock_path), os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o644) - except OSError as e: - if e.errno == errno.EEXIST: - six.raise_from( - EnvironmentError("cannot create lockfile in {path}".format(path=p)), e - ) - else: - raise - else: - pid = os.getpid() - spid = str(pid) - if not isinstance(spid, six.binary_type): - spid = spid.encode("ascii") - os.write(fd, spid) - os.close(fd) - if not lock_path.is_file(): - raise EnvironmentError("lock path got renamed after sucessfull creation") - return lock_path - - -def register_cleanup_lock_removal(lock_path, register=atexit.register): - pid = os.getpid() - - def cleanup_on_exit(lock_path=lock_path, original_pid=pid): - current_pid = os.getpid() - if current_pid != original_pid: - # fork - return - try: - lock_path.unlink() - except (OSError, IOError): - pass - - return register(cleanup_on_exit) - - -def delete_a_numbered_dir(path): - create_cleanup_lock(path) - parent = path.parent - - garbage = parent.joinpath("garbage-{}".format(uuid.uuid4())) - path.rename(garbage) - shutil.rmtree(str(garbage), ignore_errors=True) - - -def ensure_deletable(path, consider_lock_dead_if_created_before): - lock = get_lock_path(path) - if not lock.exists(): - return True - try: - lock_time = lock.stat().st_mtime - except Exception: - return False - else: - if lock_time < consider_lock_dead_if_created_before: - lock.unlink() - return True - else: - return False - - -def try_cleanup(path, consider_lock_dead_if_created_before): - if ensure_deletable(path, consider_lock_dead_if_created_before): - delete_a_numbered_dir(path) - - -def cleanup_candidates(root, prefix, keep): - max_existing = _max(map(parse_num, find_suffixes(root, prefix)), -1) - max_delete = max_existing - keep - paths = find_prefixed(root, prefix) - paths, paths2 = itertools.tee(paths) - numbers = map(parse_num, extract_suffixes(paths2, prefix)) - for path, number in zip(paths, numbers): - if number <= max_delete: - yield path - - -def cleanup_numbered_dir(root, prefix, keep, consider_lock_dead_if_created_before): - for path in cleanup_candidates(root, prefix, keep): - try_cleanup(path, consider_lock_dead_if_created_before) - for path in root.glob("garbage-*"): - try_cleanup(path, consider_lock_dead_if_created_before) - - -def make_numbered_dir_with_cleanup(root, prefix, keep, lock_timeout): - e = None - for i in range(10): - try: - p = make_numbered_dir(root, prefix) - lock_path = create_cleanup_lock(p) - register_cleanup_lock_removal(lock_path) - except Exception as e: - pass - else: - consider_lock_dead_if_created_before = p.stat().st_mtime - lock_timeout - cleanup_numbered_dir( - root=root, - prefix=prefix, - keep=keep, - consider_lock_dead_if_created_before=consider_lock_dead_if_created_before, - ) - return p - assert e is not None - raise e +from .pathlib import Path, make_numbered_dir, make_numbered_dir_with_cleanup @attr.s diff --git a/testing/test_tmpdir.py b/testing/test_tmpdir.py index 02687f6fc..2148a8efe 100644 --- a/testing/test_tmpdir.py +++ b/testing/test_tmpdir.py @@ -192,7 +192,7 @@ class TestNumberedDir(object): PREFIX = "fun-" def test_make(self, tmp_path): - from _pytest.tmpdir import make_numbered_dir + from _pytest.pathlib import make_numbered_dir for i in range(10): d = make_numbered_dir(root=tmp_path, prefix=self.PREFIX) @@ -202,7 +202,7 @@ class TestNumberedDir(object): def test_cleanup_lock_create(self, tmp_path): d = tmp_path.joinpath("test") d.mkdir() - from _pytest.tmpdir import create_cleanup_lock + from _pytest.pathlib import create_cleanup_lock lockfile = create_cleanup_lock(d) with pytest.raises(EnvironmentError, match="cannot create lockfile in .*"): @@ -211,7 +211,7 @@ class TestNumberedDir(object): lockfile.unlink() def test_lock_register_cleanup_removal(self, tmp_path): - from _pytest.tmpdir import create_cleanup_lock, register_cleanup_lock_removal + from _pytest.pathlib import create_cleanup_lock, register_cleanup_lock_removal lock = create_cleanup_lock(tmp_path) @@ -236,7 +236,7 @@ class TestNumberedDir(object): def test_cleanup_keep(self, tmp_path): self.test_make(tmp_path) - from _pytest.tmpdir import cleanup_numbered_dir + from _pytest.pathlib import cleanup_numbered_dir cleanup_numbered_dir( root=tmp_path, @@ -249,15 +249,15 @@ class TestNumberedDir(object): def test_cleanup_locked(self, tmp_path): - from _pytest import tmpdir + from _pytest import pathlib - p = tmpdir.make_numbered_dir(root=tmp_path, prefix=self.PREFIX) + p = pathlib.make_numbered_dir(root=tmp_path, prefix=self.PREFIX) - tmpdir.create_cleanup_lock(p) + pathlib.create_cleanup_lock(p) - assert not tmpdir.ensure_deletable( + assert not pathlib.ensure_deletable( p, consider_lock_dead_if_created_before=p.stat().st_mtime - 1 ) - assert tmpdir.ensure_deletable( + assert pathlib.ensure_deletable( p, consider_lock_dead_if_created_before=p.stat().st_mtime + 1 ) From 00716177b4af3dd885ea1c1cb0ccfd71a3893b51 Mon Sep 17 00:00:00 2001 From: Ronny Pfannschmidt Date: Sat, 29 Sep 2018 13:16:27 +0200 Subject: [PATCH 064/105] fix missed Path import --- testing/python/fixture.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/testing/python/fixture.py b/testing/python/fixture.py index a9e33b455..8d7b07b0d 100644 --- a/testing/python/fixture.py +++ b/testing/python/fixture.py @@ -6,7 +6,7 @@ import pytest from _pytest.pytester import get_public_names from _pytest.fixtures import FixtureLookupError, FixtureRequest from _pytest import fixtures -from _pytest.compat import Path +from _pytest.pathlib import Path def test_getfuncargnames(): From 2831cb9ab5eb5acdc2eb0e55becf507a93ea3ac0 Mon Sep 17 00:00:00 2001 From: Ronny Pfannschmidt Date: Mon, 1 Oct 2018 13:44:52 +0200 Subject: [PATCH 065/105] unify paths.py and pathlib.py --- src/_pytest/assertion/rewrite.py | 2 +- src/_pytest/cacheprovider.py | 5 ++- src/_pytest/pathlib.py | 49 ++++++++++++++++++++++++++++++ src/_pytest/paths.py | 52 -------------------------------- testing/test_paths.py | 2 +- 5 files changed, 53 insertions(+), 57 deletions(-) delete mode 100644 src/_pytest/paths.py diff --git a/src/_pytest/assertion/rewrite.py b/src/_pytest/assertion/rewrite.py index f61496050..88331dd4b 100644 --- a/src/_pytest/assertion/rewrite.py +++ b/src/_pytest/assertion/rewrite.py @@ -19,7 +19,7 @@ import py from _pytest.assertion import util from _pytest.pathlib import PurePath from _pytest.compat import spec_from_file_location -from _pytest.paths import fnmatch_ex +from _pytest.pathlib import fnmatch_ex # pytest caches rewritten pycs in __pycache__. if hasattr(imp, "get_tag"): diff --git a/src/_pytest/cacheprovider.py b/src/_pytest/cacheprovider.py index be291bbff..eaa470fbf 100755 --- a/src/_pytest/cacheprovider.py +++ b/src/_pytest/cacheprovider.py @@ -15,9 +15,8 @@ import pytest import json import shutil -from . import paths from .compat import _PY2 as PY2 -from .pathlib import Path +from .pathlib import Path, resolve_from_str README_CONTENT = u"""\ # pytest cache directory # @@ -46,7 +45,7 @@ class Cache(object): @staticmethod def cache_dir_from_config(config): - return paths.resolve_from_str(config.getini("cache_dir"), config.rootdir) + return resolve_from_str(config.getini("cache_dir"), config.rootdir) def warn(self, fmt, **args): from _pytest.warnings import _issue_config_warning diff --git a/src/_pytest/pathlib.py b/src/_pytest/pathlib.py index 2d9f1d640..a86f1e40a 100644 --- a/src/_pytest/pathlib.py +++ b/src/_pytest/pathlib.py @@ -4,14 +4,19 @@ import errno import atexit import operator import six +import sys from functools import reduce import uuid from six.moves import map import itertools import shutil +from os.path import expanduser, expandvars, isabs, sep +from posixpath import sep as posix_sep +import fnmatch from .compat import PY36 + if PY36: from pathlib import Path, PurePath else: @@ -181,3 +186,47 @@ def make_numbered_dir_with_cleanup(root, prefix, keep, lock_timeout): return p assert e is not None raise e + + +def resolve_from_str(input, root): + assert not isinstance(input, Path), "would break on py2" + root = Path(root) + input = expanduser(input) + input = expandvars(input) + if isabs(input): + return Path(input) + else: + return root.joinpath(input) + + +def fnmatch_ex(pattern, path): + """FNMatcher port from py.path.common which works with PurePath() instances. + + The difference between this algorithm and PurePath.match() is that the latter matches "**" glob expressions + for each part of the path, while this algorithm uses the whole path instead. + + For example: + "tests/foo/bar/doc/test_foo.py" matches pattern "tests/**/doc/test*.py" with this algorithm, but not with + PurePath.match(). + + This algorithm was ported to keep backward-compatibility with existing settings which assume paths match according + this logic. + + References: + * https://bugs.python.org/issue29249 + * https://bugs.python.org/issue34731 + """ + path = PurePath(path) + iswin32 = sys.platform.startswith("win") + + if iswin32 and sep not in pattern and posix_sep in pattern: + # Running on Windows, the pattern has no Windows path separators, + # and the pattern has one or more Posix path separators. Replace + # the Posix path separators with the Windows path separator. + pattern = pattern.replace(posix_sep, sep) + + if sep not in pattern: + name = path.name + else: + name = six.text_type(path) + return fnmatch.fnmatch(name, pattern) diff --git a/src/_pytest/paths.py b/src/_pytest/paths.py deleted file mode 100644 index 3507cae7f..000000000 --- a/src/_pytest/paths.py +++ /dev/null @@ -1,52 +0,0 @@ -from os.path import expanduser, expandvars, isabs, sep -from posixpath import sep as posix_sep -import fnmatch -import sys - -import six - -from .pathlib import Path, PurePath - - -def resolve_from_str(input, root): - assert not isinstance(input, Path), "would break on py2" - root = Path(root) - input = expanduser(input) - input = expandvars(input) - if isabs(input): - return Path(input) - else: - return root.joinpath(input) - - -def fnmatch_ex(pattern, path): - """FNMatcher port from py.path.common which works with PurePath() instances. - - The difference between this algorithm and PurePath.match() is that the latter matches "**" glob expressions - for each part of the path, while this algorithm uses the whole path instead. - - For example: - "tests/foo/bar/doc/test_foo.py" matches pattern "tests/**/doc/test*.py" with this algorithm, but not with - PurePath.match(). - - This algorithm was ported to keep backward-compatibility with existing settings which assume paths match according - this logic. - - References: - * https://bugs.python.org/issue29249 - * https://bugs.python.org/issue34731 - """ - path = PurePath(path) - iswin32 = sys.platform.startswith("win") - - if iswin32 and sep not in pattern and posix_sep in pattern: - # Running on Windows, the pattern has no Windows path separators, - # and the pattern has one or more Posix path separators. Replace - # the Posix path separators with the Windows path separator. - pattern = pattern.replace(posix_sep, sep) - - if sep not in pattern: - name = path.name - else: - name = six.text_type(path) - return fnmatch.fnmatch(name, pattern) diff --git a/testing/test_paths.py b/testing/test_paths.py index 2bb1335fb..2eb07bbd4 100644 --- a/testing/test_paths.py +++ b/testing/test_paths.py @@ -4,7 +4,7 @@ import py import pytest -from _pytest.paths import fnmatch_ex +from _pytest.pathlib import fnmatch_ex class TestPort: From 30369140979973c486b0c2c55f8d346db8d9f252 Mon Sep 17 00:00:00 2001 From: Ronny Pfannschmidt Date: Mon, 1 Oct 2018 16:39:24 +0200 Subject: [PATCH 066/105] sort out rmtree expectations --- src/_pytest/cacheprovider.py | 5 ++-- src/_pytest/pathlib.py | 48 ++++++++++++++++++++++++++++++++++-- src/_pytest/tmpdir.py | 12 +++++---- testing/test_tmpdir.py | 16 ++++++++++++ 4 files changed, 71 insertions(+), 10 deletions(-) diff --git a/src/_pytest/cacheprovider.py b/src/_pytest/cacheprovider.py index eaa470fbf..fd8ef8fc0 100755 --- a/src/_pytest/cacheprovider.py +++ b/src/_pytest/cacheprovider.py @@ -13,10 +13,9 @@ import attr import pytest import json -import shutil from .compat import _PY2 as PY2 -from .pathlib import Path, resolve_from_str +from .pathlib import Path, resolve_from_str, rmtree README_CONTENT = u"""\ # pytest cache directory # @@ -39,7 +38,7 @@ class Cache(object): def for_config(cls, config): cachedir = cls.cache_dir_from_config(config) if config.getoption("cacheclear") and cachedir.exists(): - shutil.rmtree(str(cachedir)) + rmtree(cachedir, force=True) cachedir.mkdir() return cls(cachedir, config) diff --git a/src/_pytest/pathlib.py b/src/_pytest/pathlib.py index a86f1e40a..cd9796973 100644 --- a/src/_pytest/pathlib.py +++ b/src/_pytest/pathlib.py @@ -13,6 +13,7 @@ import shutil from os.path import expanduser, expandvars, isabs, sep from posixpath import sep as posix_sep import fnmatch +import stat from .compat import PY36 @@ -30,7 +31,33 @@ LOCK_TIMEOUT = 60 * 60 * 3 get_lock_path = operator.methodcaller("joinpath", ".lock") +def ensure_reset_dir(path): + """ + ensures the given path is a empty directory + """ + if path.exists(): + rmtree(path, force=True) + path.mkdir() + + +def _shutil_rmtree_remove_writable(func, fspath, _): + "Clear the readonly bit and reattempt the removal" + os.chmod(fspath, stat.S_IWRITE) + func(fspath) + + +def rmtree(path, force=False): + if force: + # ignore_errors leaves dead folders around + # python needs a rm -rf as a followup + # the trick with _shutil_rmtree_remove_writable is unreliable + shutil.rmtree(str(path), ignore_errors=True) + else: + shutil.rmtree(str(path)) + + def find_prefixed(root, prefix): + """finds all elements in root that begin with the prefix, case insensitive""" l_prefix = prefix.lower() for x in root.iterdir(): if x.name.lower().startswith(l_prefix): @@ -38,16 +65,24 @@ def find_prefixed(root, prefix): def extract_suffixes(iter, prefix): + """ + :param iter: iterator over path names + :param prefix: expected prefix of the path names + :returns: the parts of the paths following the prefix + """ p_len = len(prefix) for p in iter: yield p.name[p_len:] def find_suffixes(root, prefix): + """combines find_prefixes and extract_suffixes + """ return extract_suffixes(find_prefixed(root, prefix), prefix) def parse_num(maybe_num): + """parses number path suffixes, returns -1 on error""" try: return int(maybe_num) except ValueError: @@ -55,11 +90,12 @@ def parse_num(maybe_num): def _max(iterable, default): - # needed due to python2.7 lacking the default argument for max + """needed due to python2.7 lacking the default argument for max""" return reduce(max, iterable, default) def make_numbered_dir(root, prefix): + """create a directory with a increased number as suffix for the given prefix""" for i in range(10): # try up to 10 times to create the folder max_existing = _max(map(parse_num, find_suffixes(root, prefix)), -1) @@ -80,6 +116,7 @@ def make_numbered_dir(root, prefix): def create_cleanup_lock(p): + """crates a lock to prevent premature folder cleanup""" lock_path = get_lock_path(p) try: fd = os.open(str(lock_path), os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o644) @@ -103,6 +140,7 @@ def create_cleanup_lock(p): def register_cleanup_lock_removal(lock_path, register=atexit.register): + """registers a cleanup function for removing a lock, by default on atexit""" pid = os.getpid() def cleanup_on_exit(lock_path=lock_path, original_pid=pid): @@ -119,15 +157,17 @@ def register_cleanup_lock_removal(lock_path, register=atexit.register): def delete_a_numbered_dir(path): + """removes a numbered directory""" create_cleanup_lock(path) parent = path.parent garbage = parent.joinpath("garbage-{}".format(uuid.uuid4())) path.rename(garbage) - shutil.rmtree(str(garbage), ignore_errors=True) + rmtree(garbage, force=True) def ensure_deletable(path, consider_lock_dead_if_created_before): + """checks if a lock exists and breaks it if its considered dead""" lock = get_lock_path(path) if not lock.exists(): return True @@ -144,11 +184,13 @@ def ensure_deletable(path, consider_lock_dead_if_created_before): def try_cleanup(path, consider_lock_dead_if_created_before): + """tries to cleanup a folder if we can ensure its deletable""" if ensure_deletable(path, consider_lock_dead_if_created_before): delete_a_numbered_dir(path) def cleanup_candidates(root, prefix, keep): + """lists candidates for numbered directories to be removed - follows py.path""" max_existing = _max(map(parse_num, find_suffixes(root, prefix)), -1) max_delete = max_existing - keep paths = find_prefixed(root, prefix) @@ -160,6 +202,7 @@ def cleanup_candidates(root, prefix, keep): def cleanup_numbered_dir(root, prefix, keep, consider_lock_dead_if_created_before): + """cleanup for lock driven numbered directories""" for path in cleanup_candidates(root, prefix, keep): try_cleanup(path, consider_lock_dead_if_created_before) for path in root.glob("garbage-*"): @@ -167,6 +210,7 @@ def cleanup_numbered_dir(root, prefix, keep, consider_lock_dead_if_created_befor def make_numbered_dir_with_cleanup(root, prefix, keep, lock_timeout): + """creates a numbered dir with a cleanup lock and removes old ones""" e = None for i in range(10): try: diff --git a/src/_pytest/tmpdir.py b/src/_pytest/tmpdir.py index 40a9cbf90..65562db4d 100644 --- a/src/_pytest/tmpdir.py +++ b/src/_pytest/tmpdir.py @@ -6,9 +6,13 @@ import pytest import py from _pytest.monkeypatch import MonkeyPatch import attr -import shutil import tempfile -from .pathlib import Path, make_numbered_dir, make_numbered_dir_with_cleanup +from .pathlib import ( + Path, + make_numbered_dir, + make_numbered_dir_with_cleanup, + ensure_reset_dir, +) @attr.s @@ -39,9 +43,7 @@ class TempPathFactory(object): if self._basetemp is None: if self.given_basetemp is not None: basetemp = Path(self.given_basetemp) - if basetemp.exists(): - shutil.rmtree(str(basetemp)) - basetemp.mkdir() + ensure_reset_dir(basetemp) else: temproot = Path(tempfile.gettempdir()) user = get_user() or "unknown" diff --git a/testing/test_tmpdir.py b/testing/test_tmpdir.py index 2148a8efe..db1e8b00c 100644 --- a/testing/test_tmpdir.py +++ b/testing/test_tmpdir.py @@ -261,3 +261,19 @@ class TestNumberedDir(object): assert pathlib.ensure_deletable( p, consider_lock_dead_if_created_before=p.stat().st_mtime + 1 ) + + def test_rmtree(self, tmp_path): + from _pytest.pathlib import rmtree + + adir = tmp_path / "adir" + adir.mkdir() + rmtree(adir) + + assert not adir.exists() + + adir.mkdir() + afile = adir / "afile" + afile.write_bytes(b"aa") + + rmtree(adir, force=True) + assert not adir.exists() From ad6f63edda671733d4ad08a390da3bc6c1b758d1 Mon Sep 17 00:00:00 2001 From: Ronny Pfannschmidt Date: Mon, 1 Oct 2018 21:28:42 +0200 Subject: [PATCH 067/105] add changelog --- changelog/3985.feature.rst | 1 + changelog/3988.trivial.rst | 1 + 2 files changed, 2 insertions(+) create mode 100644 changelog/3985.feature.rst create mode 100644 changelog/3988.trivial.rst diff --git a/changelog/3985.feature.rst b/changelog/3985.feature.rst new file mode 100644 index 000000000..19070cad0 --- /dev/null +++ b/changelog/3985.feature.rst @@ -0,0 +1 @@ +Introduce ``tmp_path`` as a fixture providing a Path object. diff --git a/changelog/3988.trivial.rst b/changelog/3988.trivial.rst new file mode 100644 index 000000000..876db9798 --- /dev/null +++ b/changelog/3988.trivial.rst @@ -0,0 +1 @@ +Port the implementation of tmpdir to pathlib. From 4a436b54709b4d6a530cb2931e6f51a17c97945b Mon Sep 17 00:00:00 2001 From: Ronny Pfannschmidt Date: Tue, 2 Oct 2018 08:03:58 +0200 Subject: [PATCH 068/105] resolve in code review commments --- src/_pytest/pathlib.py | 21 +++++++++++++-------- src/_pytest/tmpdir.py | 33 +++++++++++++++++++++++++-------- 2 files changed, 38 insertions(+), 16 deletions(-) diff --git a/src/_pytest/pathlib.py b/src/_pytest/pathlib.py index cd9796973..439f4d9ba 100644 --- a/src/_pytest/pathlib.py +++ b/src/_pytest/pathlib.py @@ -89,16 +89,22 @@ def parse_num(maybe_num): return -1 -def _max(iterable, default): - """needed due to python2.7 lacking the default argument for max""" - return reduce(max, iterable, default) +if six.PY2: + + def _max(iterable, default): + """needed due to python2.7 lacking the default argument for max""" + return reduce(max, iterable, default) + + +else: + _max = max def make_numbered_dir(root, prefix): """create a directory with a increased number as suffix for the given prefix""" for i in range(10): # try up to 10 times to create the folder - max_existing = _max(map(parse_num, find_suffixes(root, prefix)), -1) + max_existing = _max(map(parse_num, find_suffixes(root, prefix)), default=-1) new_number = max_existing + 1 new_path = root.joinpath("{}{}".format(prefix, new_number)) try: @@ -109,9 +115,8 @@ def make_numbered_dir(root, prefix): return new_path else: raise EnvironmentError( - "could not create numbered dir with prefix {prefix} in {root})".format( - prefix=prefix, root=root - ) + "could not create numbered dir with prefix " + "{prefix} in {root} after 10 tries".format(prefix=prefix, root=root) ) @@ -191,7 +196,7 @@ def try_cleanup(path, consider_lock_dead_if_created_before): def cleanup_candidates(root, prefix, keep): """lists candidates for numbered directories to be removed - follows py.path""" - max_existing = _max(map(parse_num, find_suffixes(root, prefix)), -1) + max_existing = _max(map(parse_num, find_suffixes(root, prefix)), default=-1) max_delete = max_existing - keep paths = find_prefixed(root, prefix) paths, paths2 = itertools.tee(paths) diff --git a/src/_pytest/tmpdir.py b/src/_pytest/tmpdir.py index 65562db4d..422f1f7fb 100644 --- a/src/_pytest/tmpdir.py +++ b/src/_pytest/tmpdir.py @@ -17,7 +17,9 @@ from .pathlib import ( @attr.s class TempPathFactory(object): - """docstring for ClassName""" + """Factory for temporary directories under the common base temp directory. + + The base directory can be configured using the ``--basetemp`` option.""" given_basetemp = attr.ib() trace = attr.ib() @@ -25,11 +27,15 @@ class TempPathFactory(object): @classmethod def from_config(cls, config): + """ + :param config: a pytest configuration + """ return cls( given_basetemp=config.option.basetemp, trace=config.trace.get("tmpdir") ) def mktemp(self, basename, numbered=True): + """makes a temporary directory managed by the factory""" if not numbered: p = self.getbasetemp().joinpath(basename) p.mkdir() @@ -64,12 +70,12 @@ class TempPathFactory(object): @attr.s class TempdirFactory(object): - """Factory for temporary directories under the common base temp directory. - - The base directory can be configured using the ``--basetemp`` option. + """ + backward comptibility wrapper that implements + :class:``py.path.local`` for :class:``TempPathFactory`` """ - tmppath_factory = attr.ib() + _tmppath_factory = attr.ib() def ensuretemp(self, string, dir=1): """ (deprecated) return temporary directory path with @@ -86,13 +92,13 @@ class TempdirFactory(object): If ``numbered``, ensure the directory is unique by adding a number prefix greater than any existing one. """ - return py.path.local(self.tmppath_factory.mktemp(basename, numbered).resolve()) + return py.path.local(self._tmppath_factory.mktemp(basename, numbered).resolve()) def getbasetemp(self): - return py.path.local(self.tmppath_factory.getbasetemp().resolve()) + return py.path.local(self._tmppath_factory.getbasetemp().resolve()) def finish(self): - self.tmppath_factory.trace("finish") + self._tmppath_factory.trace("finish") def get_user(): @@ -150,4 +156,15 @@ def tmpdir(request, tmpdir_factory): @pytest.fixture def tmp_path(tmpdir): + """Return a temporary directory path object + which is unique to each test function invocation, + created as a sub directory of the base temporary + directory. The returned object is a :class:`pathlib.Path` + object. + + .. note:: + + in python < 3.6 this is a pathlib2.Path + """ + return Path(tmpdir) From b82d6f7a0b628a5f1cb6d326ec25389f3c804abf Mon Sep 17 00:00:00 2001 From: Ronny Pfannschmidt Date: Tue, 2 Oct 2018 08:34:02 +0200 Subject: [PATCH 069/105] pytester: use per test tmproot --- src/_pytest/pytester.py | 3 +++ src/_pytest/tmpdir.py | 4 +++- testing/test_tmpdir.py | 2 -- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/src/_pytest/pytester.py b/src/_pytest/pytester.py index 85f824784..228f9a193 100644 --- a/src/_pytest/pytester.py +++ b/src/_pytest/pytester.py @@ -496,6 +496,8 @@ class Testdir(object): self._mod_collections = WeakKeyDictionary() name = request.function.__name__ self.tmpdir = tmpdir_factory.mktemp(name, numbered=True) + self.test_tmproot = tmpdir_factory.mktemp("tmp-" + name, numbered=True) + os.environ["PYTEST_DEBUG_TEMPROOT"] = str(self.test_tmproot) self.plugins = [] self._cwd_snapshot = CwdSnapshot() self._sys_path_snapshot = SysPathsSnapshot() @@ -522,6 +524,7 @@ class Testdir(object): self._sys_modules_snapshot.restore() self._sys_path_snapshot.restore() self._cwd_snapshot.restore() + del os.environ["PYTEST_DEBUG_TEMPROOT"] def __take_sys_modules_snapshot(self): # some zope modules used by twisted-related tests keep internal state diff --git a/src/_pytest/tmpdir.py b/src/_pytest/tmpdir.py index 422f1f7fb..7b5f6510e 100644 --- a/src/_pytest/tmpdir.py +++ b/src/_pytest/tmpdir.py @@ -1,6 +1,7 @@ """ support for providing temporary directories to test functions. """ from __future__ import absolute_import, division, print_function +import os import re import pytest import py @@ -51,7 +52,8 @@ class TempPathFactory(object): basetemp = Path(self.given_basetemp) ensure_reset_dir(basetemp) else: - temproot = Path(tempfile.gettempdir()) + from_env = os.environ.get("PYTEST_DEBUG_TEMPROOT") + temproot = Path(from_env or tempfile.gettempdir()) user = get_user() or "unknown" # use a sub-directory in the temproot to speed-up # make_numbered_dir() call diff --git a/testing/test_tmpdir.py b/testing/test_tmpdir.py index db1e8b00c..9244e309d 100644 --- a/testing/test_tmpdir.py +++ b/testing/test_tmpdir.py @@ -143,7 +143,6 @@ def break_getuser(monkeypatch): monkeypatch.delenv(envvar, raising=False) -@pytest.mark.skip(reason="creates random tmpdirs as part of a system level test") @pytest.mark.usefixtures("break_getuser") @pytest.mark.skipif(sys.platform.startswith("win"), reason="no os.getuid on windows") def test_tmpdir_fallback_uid_not_found(testdir): @@ -162,7 +161,6 @@ def test_tmpdir_fallback_uid_not_found(testdir): reprec.assertoutcome(passed=1) -@pytest.mark.skip(reason="creates random tmpdirs as part of a system level test") @pytest.mark.usefixtures("break_getuser") @pytest.mark.skipif(sys.platform.startswith("win"), reason="no os.getuid on windows") def test_get_user_uid_not_found(): From 94829c391bc868f40f727be7f86d16fd1c223756 Mon Sep 17 00:00:00 2001 From: Ronny Pfannschmidt Date: Tue, 2 Oct 2018 10:20:09 +0200 Subject: [PATCH 070/105] make tmpdir env cleanup idempotent --- src/_pytest/pytester.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/_pytest/pytester.py b/src/_pytest/pytester.py index 228f9a193..956e00087 100644 --- a/src/_pytest/pytester.py +++ b/src/_pytest/pytester.py @@ -524,7 +524,7 @@ class Testdir(object): self._sys_modules_snapshot.restore() self._sys_path_snapshot.restore() self._cwd_snapshot.restore() - del os.environ["PYTEST_DEBUG_TEMPROOT"] + os.environ.pop("PYTEST_DEBUG_TEMPROOT", None) def __take_sys_modules_snapshot(self): # some zope modules used by twisted-related tests keep internal state From ebd597b2fdba6faca1645c75cf3cf82849c76715 Mon Sep 17 00:00:00 2001 From: Ronny Pfannschmidt Date: Tue, 2 Oct 2018 20:29:33 +0200 Subject: [PATCH 071/105] use the constant for lock timeouts --- src/_pytest/tmpdir.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/_pytest/tmpdir.py b/src/_pytest/tmpdir.py index 7b5f6510e..cc38dcbf2 100644 --- a/src/_pytest/tmpdir.py +++ b/src/_pytest/tmpdir.py @@ -13,6 +13,7 @@ from .pathlib import ( make_numbered_dir, make_numbered_dir_with_cleanup, ensure_reset_dir, + LOCK_TIMEOUT, ) @@ -60,7 +61,7 @@ class TempPathFactory(object): rootdir = temproot.joinpath("pytest-of-{}".format(user)) rootdir.mkdir(exist_ok=True) basetemp = make_numbered_dir_with_cleanup( - prefix="pytest-", root=rootdir, keep=3, lock_timeout=10000 + prefix="pytest-", root=rootdir, keep=3, lock_timeout=LOCK_TIMEOUT ) assert basetemp is not None self._basetemp = t = basetemp From 36c2a101cbf047df9303c57c94a1f2cae7282f52 Mon Sep 17 00:00:00 2001 From: Ronny Pfannschmidt Date: Tue, 2 Oct 2018 20:46:09 +0200 Subject: [PATCH 072/105] add missing docstring --- src/_pytest/tmpdir.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/_pytest/tmpdir.py b/src/_pytest/tmpdir.py index cc38dcbf2..520a01a8e 100644 --- a/src/_pytest/tmpdir.py +++ b/src/_pytest/tmpdir.py @@ -98,6 +98,7 @@ class TempdirFactory(object): return py.path.local(self._tmppath_factory.mktemp(basename, numbered).resolve()) def getbasetemp(self): + """backward compat wrapper for ``_tmppath_factory.getbasetemp``""" return py.path.local(self._tmppath_factory.getbasetemp().resolve()) def finish(self): From 16e2737da3e25a076d61166f2c341db5ee27e977 Mon Sep 17 00:00:00 2001 From: Ronny Pfannschmidt Date: Thu, 11 Oct 2018 09:41:37 +0200 Subject: [PATCH 073/105] implement tmp_path_factory and deprecate pytest.ensuretemp as intended --- src/_pytest/deprecated.py | 5 +++++ src/_pytest/tmpdir.py | 40 +++++++++++++++++++++++++-------------- 2 files changed, 31 insertions(+), 14 deletions(-) diff --git a/src/_pytest/deprecated.py b/src/_pytest/deprecated.py index 69beeab5f..19c2d2818 100644 --- a/src/_pytest/deprecated.py +++ b/src/_pytest/deprecated.py @@ -109,3 +109,8 @@ PYTEST_PLUGINS_FROM_NON_TOP_LEVEL_CONFTEST = RemovedInPytest4Warning( PYTEST_NAMESPACE = RemovedInPytest4Warning( "pytest_namespace is deprecated and will be removed soon" ) + +PYTEST_ENSURETEMP = RemovedInPytest4Warning( + "pytest/tmpdir_factory.ensuretemp is deprecated, \n" + "please use the tmp_path fixture or tmp_path_factory.mktemp" +) diff --git a/src/_pytest/tmpdir.py b/src/_pytest/tmpdir.py index 520a01a8e..e86994b8e 100644 --- a/src/_pytest/tmpdir.py +++ b/src/_pytest/tmpdir.py @@ -8,6 +8,8 @@ import py from _pytest.monkeypatch import MonkeyPatch import attr import tempfile +import warnings + from .pathlib import ( Path, make_numbered_dir, @@ -88,6 +90,9 @@ class TempdirFactory(object): and is guaranteed to be empty. """ # py.log._apiwarn(">1.1", "use tmpdir function argument") + from .deprecated import PYTEST_ENSURETEMP + + warnings.warn(PYTEST_ENSURETEMP, stacklevel=2) return self.getbasetemp().ensure(string, dir=dir) def mktemp(self, basename, numbered=True): @@ -101,9 +106,6 @@ class TempdirFactory(object): """backward compat wrapper for ``_tmppath_factory.getbasetemp``""" return py.path.local(self._tmppath_factory.getbasetemp().resolve()) - def finish(self): - self._tmppath_factory.trace("finish") - def get_user(): """Return the current user name, or None if getuser() does not work @@ -127,18 +129,34 @@ def pytest_configure(config): mp = MonkeyPatch() tmppath_handler = TempPathFactory.from_config(config) t = TempdirFactory(tmppath_handler) - config._cleanup.extend([mp.undo, t.finish]) + config._cleanup.append(mp.undo) + mp.setattr(config, "_tmp_path_factory", tmppath_handler, raising=False) mp.setattr(config, "_tmpdirhandler", t, raising=False) mp.setattr(pytest, "ensuretemp", t.ensuretemp, raising=False) @pytest.fixture(scope="session") def tmpdir_factory(request): - """Return a TempdirFactory instance for the test session. + """Return a :class:`_pytest.tmpdir.TempdirFactory` instance for the test session. """ return request.config._tmpdirhandler +@pytest.fixture(scope="session") +def tmp_path_factory(request): + """Return a :class:`_pytest.tmpdir.TempPathFactory` instance for the test session. + """ + return request.config._tmp_path_factory + + +def _mk_tmp(request, factory): + name = request.node.name + name = re.sub(r"[\W]", "_", name) + MAXVAL = 30 + name = name[:MAXVAL] + return factory.mktemp(name, numbered=True) + + @pytest.fixture def tmpdir(request, tmpdir_factory): """Return a temporary directory path object @@ -149,17 +167,11 @@ def tmpdir(request, tmpdir_factory): .. _`py.path.local`: https://py.readthedocs.io/en/latest/path.html """ - name = request.node.name - name = re.sub(r"[\W]", "_", name) - MAXVAL = 30 - if len(name) > MAXVAL: - name = name[:MAXVAL] - x = tmpdir_factory.mktemp(name, numbered=True) - return x + return _mk_tmp(request, tmpdir_factory) @pytest.fixture -def tmp_path(tmpdir): +def tmp_path(request, tmp_path_factory): """Return a temporary directory path object which is unique to each test function invocation, created as a sub directory of the base temporary @@ -171,4 +183,4 @@ def tmp_path(tmpdir): in python < 3.6 this is a pathlib2.Path """ - return Path(tmpdir) + return _mk_tmp(request, tmp_path_factory) From 584051aa90b3437bcbd509a6591393e7e4d1569d Mon Sep 17 00:00:00 2001 From: Ronny Pfannschmidt Date: Thu, 11 Oct 2018 10:33:59 +0200 Subject: [PATCH 074/105] extend docs with basics about tmp_path and tmp_path_facotry --- doc/en/tmpdir.rst | 45 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 45 insertions(+) diff --git a/doc/en/tmpdir.rst b/doc/en/tmpdir.rst index 421b4c898..1c815c8d1 100644 --- a/doc/en/tmpdir.rst +++ b/doc/en/tmpdir.rst @@ -5,6 +5,51 @@ Temporary directories and files ================================================ +The ``tmp_path`` fixture +---------------------- + +.. versionadded:: 3.9 + + +You can use the ``tmpdir`` fixture which will +provide a temporary directory unique to the test invocation, +created in the `base temporary directory`_. + +``tmpdir`` is a `pathlib/pathlib2.Path`_ object. Here is an example test usage:: + + # content of test_tmp_path.py + import os + CONTENT = u"content" + + def test_create_file(tmp_path): + d = tmp_path / "sub" + d.mkdir() + p = d / "hello.txt" + p.write_text(CONTENT) + assert p.read_text() == CONTENT + assert len(tmpdir.listdir()) == 1 + assert 0 + +Running this would result in a passed test except for the last +``assert 0`` line which we use to look at values:: + + $ pytest test_tmp_path.py + ... #fill fom regendoc + + + +The ``tmp_path_facotry`` fixture +------------------------------ + +.. versionadded:: 3.9 + + +The ``tmp_path_facotry`` is a session-scoped fixture which can be used +to create arbitrary temporary directories from any other fixture or test. + +its intended to replace ``tmpdir_factory`` + + The 'tmpdir' fixture -------------------- From 4736b2bdfb99f66f998cdc387627d64fd38447d1 Mon Sep 17 00:00:00 2001 From: Ronny Pfannschmidt Date: Thu, 11 Oct 2018 15:33:19 +0200 Subject: [PATCH 075/105] address review comments --- .pre-commit-config.yaml | 2 +- changelog/3988.deprecation.rst | 1 + doc/en/tmpdir.rst | 14 +++++++++----- src/_pytest/tmpdir.py | 12 ++++++------ 4 files changed, 17 insertions(+), 12 deletions(-) create mode 100644 changelog/3988.deprecation.rst diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index fc6d9e10c..e5cc56230 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -43,6 +43,6 @@ repos: - id: changelogs-rst name: changelog filenames language: fail - entry: 'changelog files must be named ####.(feature|bugfix|doc|removal|vendor|trivial).rst' + entry: 'changelog files must be named ####.(feature|bugfix|doc|deprecation|removal|vendor|trivial).rst' exclude: changelog/(\d+\.(feature|bugfix|doc|deprecation|removal|vendor|trivial).rst|README.rst|_template.rst) files: ^changelog/ diff --git a/changelog/3988.deprecation.rst b/changelog/3988.deprecation.rst new file mode 100644 index 000000000..b731112e4 --- /dev/null +++ b/changelog/3988.deprecation.rst @@ -0,0 +1 @@ +Add a Deprecation warning for pytest.ensuretemp as it was deprecated since a while. diff --git a/doc/en/tmpdir.rst b/doc/en/tmpdir.rst index 1c815c8d1..728621152 100644 --- a/doc/en/tmpdir.rst +++ b/doc/en/tmpdir.rst @@ -6,7 +6,7 @@ Temporary directories and files ================================================ The ``tmp_path`` fixture ----------------------- +------------------------ .. versionadded:: 3.9 @@ -15,12 +15,16 @@ You can use the ``tmpdir`` fixture which will provide a temporary directory unique to the test invocation, created in the `base temporary directory`_. -``tmpdir`` is a `pathlib/pathlib2.Path`_ object. Here is an example test usage:: +``tmpdir`` is a ``pathlib/pathlib2.Path`` object. Here is an example test usage: + +.. code-block:: python # content of test_tmp_path.py import os + CONTENT = u"content" + def test_create_file(tmp_path): d = tmp_path / "sub" d.mkdir() @@ -38,8 +42,8 @@ Running this would result in a passed test except for the last -The ``tmp_path_facotry`` fixture ------------------------------- +The ``tmp_path_factory`` fixture +-------------------------------- .. versionadded:: 3.9 @@ -47,7 +51,7 @@ The ``tmp_path_facotry`` fixture The ``tmp_path_facotry`` is a session-scoped fixture which can be used to create arbitrary temporary directories from any other fixture or test. -its intended to replace ``tmpdir_factory`` +its intended to replace ``tmpdir_factory`` and returns :class:`pathlib.Path` instances. The 'tmpdir' fixture diff --git a/src/_pytest/tmpdir.py b/src/_pytest/tmpdir.py index e86994b8e..1963f14c0 100644 --- a/src/_pytest/tmpdir.py +++ b/src/_pytest/tmpdir.py @@ -25,8 +25,8 @@ class TempPathFactory(object): The base directory can be configured using the ``--basetemp`` option.""" - given_basetemp = attr.ib() - trace = attr.ib() + _given_basetemp = attr.ib() + _trace = attr.ib() _basetemp = attr.ib(default=None) @classmethod @@ -45,14 +45,14 @@ class TempPathFactory(object): p.mkdir() else: p = make_numbered_dir(root=self.getbasetemp(), prefix=basename) - self.trace("mktemp", p) + self._trace("mktemp", p) return p def getbasetemp(self): """ return base temporary directory. """ if self._basetemp is None: - if self.given_basetemp is not None: - basetemp = Path(self.given_basetemp) + if self._given_basetemp is not None: + basetemp = Path(self._given_basetemp) ensure_reset_dir(basetemp) else: from_env = os.environ.get("PYTEST_DEBUG_TEMPROOT") @@ -67,7 +67,7 @@ class TempPathFactory(object): ) assert basetemp is not None self._basetemp = t = basetemp - self.trace("new basetemp", t) + self._trace("new basetemp", t) return t else: return self._basetemp From 36dc6718435c423f768df8334e93d7f06d752784 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Fri, 12 Oct 2018 09:37:54 -0300 Subject: [PATCH 076/105] New ExceptionInfo.getrepr 'chain' parameter to be able to suppress chained exceptions --- src/_pytest/_code/code.py | 42 ++++++++++++++++++++++++++++++------ testing/code/test_excinfo.py | 20 ++++++++++++----- 2 files changed, 50 insertions(+), 12 deletions(-) diff --git a/src/_pytest/_code/code.py b/src/_pytest/_code/code.py index 2662e4320..b2321c02d 100644 --- a/src/_pytest/_code/code.py +++ b/src/_pytest/_code/code.py @@ -451,13 +451,35 @@ class ExceptionInfo(object): tbfilter=True, funcargs=False, truncate_locals=True, + chain=True, ): - """ return str()able representation of this exception info. - showlocals: show locals per traceback entry - style: long|short|no|native traceback style - tbfilter: hide entries (where __tracebackhide__ is true) + """ + Return str()able representation of this exception info. - in case of style==native, tbfilter and showlocals is ignored. + :param bool showlocals: + Show locals per traceback entry. + Ignored if ``style=="native"``. + + :param str style: long|short|no|native traceback style + + :param bool abspath: + If paths should be changed to absolute or left unchanged. + + :param bool tbfilter: + Hide entries that contain a local variable ``__tracebackhide__==True``. + Ignored if ``style=="native"``. + + :param bool funcargs: + Show fixtures ("funcargs" for legacy purposes) per traceback entry. + + :param bool truncate_locals: + With ``showlocals==True``, make sure locals can be safely represented as strings. + + :param bool chain: if chained exceptions in Python 3 should be shown. + + .. versionchanged:: 3.9 + + Added the ``chain`` parameter. """ if style == "native": return ReprExceptionInfo( @@ -476,6 +498,7 @@ class ExceptionInfo(object): tbfilter=tbfilter, funcargs=funcargs, truncate_locals=truncate_locals, + chain=chain, ) return fmt.repr_excinfo(self) @@ -516,6 +539,7 @@ class FormattedExcinfo(object): tbfilter = attr.ib(default=True) funcargs = attr.ib(default=False) truncate_locals = attr.ib(default=True) + chain = attr.ib(default=True) astcache = attr.ib(default=attr.Factory(dict), init=False, repr=False) def _getindent(self, source): @@ -735,7 +759,7 @@ class FormattedExcinfo(object): reprcrash = None repr_chain += [(reprtraceback, reprcrash, descr)] - if e.__cause__ is not None: + if e.__cause__ is not None and self.chain: e = e.__cause__ excinfo = ( ExceptionInfo((type(e), e, e.__traceback__)) @@ -743,7 +767,11 @@ class FormattedExcinfo(object): else None ) descr = "The above exception was the direct cause of the following exception:" - elif e.__context__ is not None and not e.__suppress_context__: + elif ( + e.__context__ is not None + and not e.__suppress_context__ + and self.chain + ): e = e.__context__ excinfo = ( ExceptionInfo((type(e), e, e.__traceback__)) diff --git a/testing/code/test_excinfo.py b/testing/code/test_excinfo.py index 72b1a78ab..92460cd29 100644 --- a/testing/code/test_excinfo.py +++ b/testing/code/test_excinfo.py @@ -1184,20 +1184,28 @@ raise ValueError() assert tw.lines[47] == ":15: AttributeError" @pytest.mark.skipif("sys.version_info[0] < 3") - def test_exc_repr_with_raise_from_none_chain_suppression(self, importasmod): + @pytest.mark.parametrize("mode", ["from_none", "explicit_suppress"]) + def test_exc_repr_chain_suppression(self, importasmod, mode): + """Check that exc repr does not show chained exceptions in Python 3. + - When the exception is raised with "from None" + - Explicitly suppressed with "chain=False" to ExceptionInfo.getrepr(). + """ + raise_suffix = " from None" if mode == "from_none" else "" mod = importasmod( """ def f(): try: g() except Exception: - raise AttributeError() from None + raise AttributeError(){raise_suffix} def g(): raise ValueError() - """ + """.format( + raise_suffix=raise_suffix + ) ) excinfo = pytest.raises(AttributeError, mod.f) - r = excinfo.getrepr(style="long") + r = excinfo.getrepr(style="long", chain=mode != "explicit_suppress") tw = TWMock() r.toterminal(tw) for line in tw.lines: @@ -1207,7 +1215,9 @@ raise ValueError() assert tw.lines[2] == " try:" assert tw.lines[3] == " g()" assert tw.lines[4] == " except Exception:" - assert tw.lines[5] == "> raise AttributeError() from None" + assert tw.lines[5] == "> raise AttributeError(){}".format( + raise_suffix + ) assert tw.lines[6] == "E AttributeError" assert tw.lines[7] == "" line = tw.get_write_msg(8) From 8e11fe530487e560ac52f49ec50bda1f49718554 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Fri, 12 Oct 2018 10:01:30 -0300 Subject: [PATCH 077/105] Improve tracebacks for ImportErrors in conftest.py files Fix #3332 --- changelog/3332.feature.rst | 4 ++++ src/_pytest/config/__init__.py | 18 ++++++++++++++++-- testing/acceptance_test.py | 30 +++++++++++++++++++++++++----- 3 files changed, 45 insertions(+), 7 deletions(-) create mode 100644 changelog/3332.feature.rst diff --git a/changelog/3332.feature.rst b/changelog/3332.feature.rst new file mode 100644 index 000000000..e0110c451 --- /dev/null +++ b/changelog/3332.feature.rst @@ -0,0 +1,4 @@ +Improve the error displayed when a ``conftest.py`` file could not be imported. + +In order to implement this, a new ``chain`` parameter was added to ``ExceptionInfo.getrepr`` +to show or hide chained tracebacks in Python 3 (defaults to ``True``). diff --git a/src/_pytest/config/__init__.py b/src/_pytest/config/__init__.py index efc5e1235..363412abb 100644 --- a/src/_pytest/config/__init__.py +++ b/src/_pytest/config/__init__.py @@ -57,10 +57,24 @@ def main(args=None, plugins=None): try: config = _prepareconfig(args, plugins) except ConftestImportFailure as e: + from _pytest._code import ExceptionInfo + + exc_info = ExceptionInfo(e.excinfo) tw = py.io.TerminalWriter(sys.stderr) - for line in traceback.format_exception(*e.excinfo): + tw.line( + "ImportError while loading conftest '{e.path}'.".format(e=e), red=True + ) + from _pytest.python import filter_traceback + + exc_info.traceback = exc_info.traceback.filter(filter_traceback) + exc_repr = ( + exc_info.getrepr(style="short", chain=False) + if exc_info.traceback + else exc_info.exconly() + ) + formatted_tb = safe_str(exc_repr) + for line in formatted_tb.splitlines(): tw.line(line.rstrip(), red=True) - tw.line("ERROR: could not load %s\n" % (e.path,), red=True) return 4 else: try: diff --git a/testing/acceptance_test.py b/testing/acceptance_test.py index 332af27b5..1728f1ff4 100644 --- a/testing/acceptance_test.py +++ b/testing/acceptance_test.py @@ -133,9 +133,16 @@ class TestGeneralUsage(object): assert result.ret result.stderr.fnmatch_lines(["*ERROR: not found:*{}".format(p2.basename)]) - def test_issue486_better_reporting_on_conftest_load_failure(self, testdir): + def test_better_reporting_on_conftest_load_failure(self, testdir, request): + """Show a user-friendly traceback on conftest import failures (#486, #3332)""" testdir.makepyfile("") - testdir.makeconftest("import qwerty") + testdir.makeconftest( + """ + def foo(): + import qwerty + foo() + """ + ) result = testdir.runpytest("--help") result.stdout.fnmatch_lines( """ @@ -144,10 +151,23 @@ class TestGeneralUsage(object): """ ) result = testdir.runpytest() + dirname = request.node.name + "0" + exc_name = ( + "ModuleNotFoundError" if sys.version_info >= (3, 6) else "ImportError" + ) result.stderr.fnmatch_lines( - """ - *ERROR*could not load*conftest.py* - """ + [ + "ImportError while loading conftest '*{sep}{dirname}{sep}conftest.py'.".format( + dirname=dirname, sep=os.sep + ), + "conftest.py:3: in ", + " foo()", + "conftest.py:2: in foo", + " import qwerty", + "E {}: No module named {q}qwerty{q}".format( + exc_name, q="'" if six.PY3 else "" + ), + ] ) def test_early_skip(self, testdir): From 2cb35346792af2eb22c719be07dee409c9ef5ba8 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Fri, 12 Oct 2018 10:19:50 -0300 Subject: [PATCH 078/105] Move filter_traceback to _pytest._code --- src/_pytest/_code/__init__.py | 1 + src/_pytest/_code/code.py | 35 ++++++++++++++++++++++++++++++++++ src/_pytest/config/__init__.py | 5 +---- src/_pytest/python.py | 33 +------------------------------- 4 files changed, 38 insertions(+), 36 deletions(-) diff --git a/src/_pytest/_code/__init__.py b/src/_pytest/_code/__init__.py index 815c13b42..7885d51de 100644 --- a/src/_pytest/_code/__init__.py +++ b/src/_pytest/_code/__init__.py @@ -4,6 +4,7 @@ from .code import Code # noqa from .code import ExceptionInfo # noqa from .code import Frame # noqa from .code import Traceback # noqa +from .code import filter_traceback # noqa from .code import getrawcode # noqa from .source import Source # noqa from .source import compile_ as compile # noqa diff --git a/src/_pytest/_code/code.py b/src/_pytest/_code/code.py index b2321c02d..c0f6d21a2 100644 --- a/src/_pytest/_code/code.py +++ b/src/_pytest/_code/code.py @@ -6,8 +6,10 @@ import traceback from inspect import CO_VARARGS, CO_VARKEYWORDS import attr +import pluggy import re from weakref import ref +import _pytest from _pytest.compat import _PY2, _PY3, PY35, safe_str from six import text_type import py @@ -1007,3 +1009,36 @@ else: return "maximum recursion depth exceeded" in str(excinfo.value) except UnicodeError: return False + + +# relative paths that we use to filter traceback entries from appearing to the user; +# see filter_traceback +# note: if we need to add more paths than what we have now we should probably use a list +# for better maintenance + +_PLUGGY_DIR = py.path.local(pluggy.__file__.rstrip("oc")) +# pluggy is either a package or a single module depending on the version +if _PLUGGY_DIR.basename == "__init__.py": + _PLUGGY_DIR = _PLUGGY_DIR.dirpath() +_PYTEST_DIR = py.path.local(_pytest.__file__).dirpath() +_PY_DIR = py.path.local(py.__file__).dirpath() + + +def filter_traceback(entry): + """Return True if a TracebackEntry instance should be removed from tracebacks: + * dynamically generated code (no code to show up for it); + * internal traceback from pytest or its internal libraries, py and pluggy. + """ + # entry.path might sometimes return a str object when the entry + # points to dynamically generated code + # see https://bitbucket.org/pytest-dev/py/issues/71 + raw_filename = entry.frame.code.raw.co_filename + is_generated = "<" in raw_filename and ">" in raw_filename + if is_generated: + return False + # entry.path might point to a non-existing file, in which case it will + # also return a str object. see #1133 + p = py.path.local(entry.path) + return ( + not p.relto(_PLUGGY_DIR) and not p.relto(_PYTEST_DIR) and not p.relto(_PY_DIR) + ) diff --git a/src/_pytest/config/__init__.py b/src/_pytest/config/__init__.py index 363412abb..3a3e064a9 100644 --- a/src/_pytest/config/__init__.py +++ b/src/_pytest/config/__init__.py @@ -19,6 +19,7 @@ import _pytest._code import _pytest.hookspec # the extension point definitions import _pytest.assertion from pluggy import PluginManager, HookimplMarker, HookspecMarker +from _pytest._code import ExceptionInfo, filter_traceback from _pytest.compat import safe_str from .exceptions import UsageError, PrintHelp from .findpaths import determine_setup, exists @@ -57,15 +58,11 @@ def main(args=None, plugins=None): try: config = _prepareconfig(args, plugins) except ConftestImportFailure as e: - from _pytest._code import ExceptionInfo - exc_info = ExceptionInfo(e.excinfo) tw = py.io.TerminalWriter(sys.stderr) tw.line( "ImportError while loading conftest '{e.path}'.".format(e=e), red=True ) - from _pytest.python import filter_traceback - exc_info.traceback = exc_info.traceback.filter(filter_traceback) exc_repr = ( exc_info.getrepr(style="short", chain=False) diff --git a/src/_pytest/python.py b/src/_pytest/python.py index e9d05666f..8337c7469 100644 --- a/src/_pytest/python.py +++ b/src/_pytest/python.py @@ -16,7 +16,7 @@ from _pytest.main import FSHookProxy from _pytest.config import hookimpl import _pytest -import pluggy +from _pytest._code import filter_traceback from _pytest import fixtures from _pytest import nodes from _pytest import deprecated @@ -46,37 +46,6 @@ from _pytest.mark.structures import ( ) from _pytest.warning_types import RemovedInPytest4Warning, PytestWarning -# relative paths that we use to filter traceback entries from appearing to the user; -# see filter_traceback -# note: if we need to add more paths than what we have now we should probably use a list -# for better maintenance -_pluggy_dir = py.path.local(pluggy.__file__.rstrip("oc")) -# pluggy is either a package or a single module depending on the version -if _pluggy_dir.basename == "__init__.py": - _pluggy_dir = _pluggy_dir.dirpath() -_pytest_dir = py.path.local(_pytest.__file__).dirpath() -_py_dir = py.path.local(py.__file__).dirpath() - - -def filter_traceback(entry): - """Return True if a TracebackEntry instance should be removed from tracebacks: - * dynamically generated code (no code to show up for it); - * internal traceback from pytest or its internal libraries, py and pluggy. - """ - # entry.path might sometimes return a str object when the entry - # points to dynamically generated code - # see https://bitbucket.org/pytest-dev/py/issues/71 - raw_filename = entry.frame.code.raw.co_filename - is_generated = "<" in raw_filename and ">" in raw_filename - if is_generated: - return False - # entry.path might point to a non-existing file, in which case it will - # also return a str object. see #1133 - p = py.path.local(entry.path) - return ( - not p.relto(_pluggy_dir) and not p.relto(_pytest_dir) and not p.relto(_py_dir) - ) - def pyobj_property(name): def get(self): From ef97121d4282c516e8e355a3c75cd42169848c19 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Fri, 12 Oct 2018 10:57:13 -0300 Subject: [PATCH 079/105] Removed unused ConftestImportFailure.__str__ method --- src/_pytest/config/__init__.py | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/src/_pytest/config/__init__.py b/src/_pytest/config/__init__.py index 3a3e064a9..88cbf14ba 100644 --- a/src/_pytest/config/__init__.py +++ b/src/_pytest/config/__init__.py @@ -3,7 +3,6 @@ from __future__ import absolute_import, division, print_function import argparse import inspect import shlex -import traceback import types import warnings import copy @@ -27,9 +26,6 @@ from .findpaths import determine_setup, exists hookimpl = HookimplMarker("pytest") hookspec = HookspecMarker("pytest") -# pytest startup -# - class ConftestImportFailure(Exception): def __init__(self, path, excinfo): @@ -37,12 +33,6 @@ class ConftestImportFailure(Exception): self.path = path self.excinfo = excinfo - def __str__(self): - etype, evalue, etb = self.excinfo - formatted = traceback.format_tb(etb) - # The level of the tracebacks we want to print is hand crafted :( - return repr(evalue) + "\n" + "".join(formatted[2:]) - def main(args=None, plugins=None): """ return exit code, after performing an in-process test run. From 307fa7a42ae71a345b72ce904e0c66b2e639f011 Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Wed, 19 Sep 2018 16:58:05 +0200 Subject: [PATCH 080/105] tox.ini: use testpaths, cleanup other pytest options --- tox.ini | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tox.ini b/tox.ini index 1c77f989d..ffa2eb524 100644 --- a/tox.ini +++ b/tox.ini @@ -201,13 +201,13 @@ commands = python scripts/release.py {posargs} [pytest] minversion = 2.0 -plugins = pytester -addopts = -ra -p pytester --ignore=testing/cx_freeze -rsyncdirs = tox.ini pytest.py _pytest testing +addopts = -ra -p pytester +rsyncdirs = tox.ini doc src testing python_files = test_*.py *_test.py testing/*/*.py python_classes = Test Acceptance python_functions = test -norecursedirs = .tox ja .hg cx_freeze_source testing/example_scripts +testpaths = doc,testing +norecursedirs = testing/example_scripts xfail_strict=true filterwarnings = error From 7268462b33954e756cfaaa13da81c3c84c2971ee Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Wed, 10 Oct 2018 16:56:18 +0200 Subject: [PATCH 081/105] Resolve symlinks for args This fixes running `pytest tests/test_foo.py::test_bar`, where `tests` is a symlink to `project/app/tests`: previously `project/app/conftest.py` would be ignored for fixtures then. --- changelog/4108.bugfix.rst | 5 ++++ src/_pytest/fixtures.py | 2 +- src/_pytest/main.py | 2 +- testing/acceptance_test.py | 24 +++++++++++++------ testing/test_conftest.py | 48 +++++++++++++++++++++++++++++++++++++- 5 files changed, 71 insertions(+), 10 deletions(-) create mode 100644 changelog/4108.bugfix.rst diff --git a/changelog/4108.bugfix.rst b/changelog/4108.bugfix.rst new file mode 100644 index 000000000..d136d5996 --- /dev/null +++ b/changelog/4108.bugfix.rst @@ -0,0 +1,5 @@ +Resolve symbolic links for args. + +This fixes running ``pytest tests/test_foo.py::test_bar``, where ``tests`` +is a symlink to ``project/app/tests``: +previously ``project/app/conftest.py`` would be ignored for fixtures then. diff --git a/src/_pytest/fixtures.py b/src/_pytest/fixtures.py index 3dec56a35..1829cf411 100644 --- a/src/_pytest/fixtures.py +++ b/src/_pytest/fixtures.py @@ -1175,7 +1175,7 @@ class FixtureManager(object): def pytest_plugin_registered(self, plugin): nodeid = None try: - p = py.path.local(plugin.__file__) + p = py.path.local(plugin.__file__).realpath() except AttributeError: pass else: diff --git a/src/_pytest/main.py b/src/_pytest/main.py index bf4faaf6a..0171bda0c 100644 --- a/src/_pytest/main.py +++ b/src/_pytest/main.py @@ -490,7 +490,7 @@ class Session(nodes.FSCollector): from _pytest.python import Package names = self._parsearg(arg) - argpath = names.pop(0) + argpath = names.pop(0).realpath() paths = [] root = self diff --git a/testing/acceptance_test.py b/testing/acceptance_test.py index e324f55ab..2a6c75740 100644 --- a/testing/acceptance_test.py +++ b/testing/acceptance_test.py @@ -753,16 +753,26 @@ class TestInvocationVariants(object): monkeypatch.syspath_prepend(p) # module picked up in symlink-ed directory: + # It picks up local/lib/foo/bar (symlink) via sys.path. result = testdir.runpytest("--pyargs", "-v", "foo.bar") testdir.chdir() assert result.ret == 0 - result.stdout.fnmatch_lines( - [ - "*lib/foo/bar/test_bar.py::test_bar*PASSED*", - "*lib/foo/bar/test_bar.py::test_other*PASSED*", - "*2 passed*", - ] - ) + if hasattr(py.path.local, "mksymlinkto"): + result.stdout.fnmatch_lines( + [ + "lib/foo/bar/test_bar.py::test_bar <- local/lib/foo/bar/test_bar.py PASSED*", + "lib/foo/bar/test_bar.py::test_other <- local/lib/foo/bar/test_bar.py PASSED*", + "*2 passed*", + ] + ) + else: + result.stdout.fnmatch_lines( + [ + "local/lib/foo/bar/test_bar.py::test_bar PASSED*", + "local/lib/foo/bar/test_bar.py::test_other PASSED*", + "*2 passed*", + ] + ) def test_cmdline_python_package_not_exists(self, testdir): result = testdir.runpytest("--pyargs", "tpkgwhatv") diff --git a/testing/test_conftest.py b/testing/test_conftest.py index 07da5d5ee..a2df0ae37 100644 --- a/testing/test_conftest.py +++ b/testing/test_conftest.py @@ -4,7 +4,7 @@ import textwrap import py import pytest from _pytest.config import PytestPluginManager -from _pytest.main import EXIT_NOTESTSCOLLECTED, EXIT_USAGEERROR +from _pytest.main import EXIT_NOTESTSCOLLECTED, EXIT_OK, EXIT_USAGEERROR @pytest.fixture(scope="module", params=["global", "inpackage"]) @@ -186,6 +186,52 @@ def test_conftest_confcutdir(testdir): assert "warning: could not load initial" not in result.stdout.str() +@pytest.mark.skipif( + not hasattr(py.path.local, "mksymlinkto"), + reason="symlink not available on this platform", +) +def test_conftest_symlink(testdir): + """Ensure that conftest.py is used for resolved symlinks.""" + realtests = testdir.tmpdir.mkdir("real").mkdir("app").mkdir("tests") + testdir.tmpdir.join("symlinktests").mksymlinkto(realtests) + testdir.makepyfile( + **{ + "real/app/tests/test_foo.py": "def test1(fixture): pass", + "real/conftest.py": textwrap.dedent( + """ + import pytest + + print("conftest_loaded") + + @pytest.fixture + def fixture(): + print("fixture_used") + """ + ), + } + ) + result = testdir.runpytest("-vs", "symlinktests") + result.stdout.fnmatch_lines( + [ + "*conftest_loaded*", + "real/app/tests/test_foo.py::test1 fixture_used", + "PASSED", + ] + ) + assert result.ret == EXIT_OK + + realtests.ensure("__init__.py") + result = testdir.runpytest("-vs", "symlinktests/test_foo.py::test1") + result.stdout.fnmatch_lines( + [ + "*conftest_loaded*", + "real/app/tests/test_foo.py::test1 fixture_used", + "PASSED", + ] + ) + assert result.ret == EXIT_OK + + def test_no_conftest(testdir): testdir.makeconftest("assert 0") result = testdir.runpytest("--noconftest") From 323c846ce647fe2421847a1e163c873983321fe3 Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Sun, 14 Oct 2018 14:47:40 +0200 Subject: [PATCH 082/105] tox.ini: fix testpaths Broken recently in 307fa7a4 (features branch). [ci skip] (since it apparently is not tested/used there) --- tox.ini | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tox.ini b/tox.ini index ffa2eb524..1985942d0 100644 --- a/tox.ini +++ b/tox.ini @@ -206,7 +206,8 @@ rsyncdirs = tox.ini doc src testing python_files = test_*.py *_test.py testing/*/*.py python_classes = Test Acceptance python_functions = test -testpaths = doc,testing +# NOTE: "doc" is not included here, but gets tested explicitly via "doctesting". +testpaths = testing norecursedirs = testing/example_scripts xfail_strict=true filterwarnings = From d7be039f1b6ed1f3aaf04283a50e4601c25d1115 Mon Sep 17 00:00:00 2001 From: Jose Carlos Menezes Date: Sun, 14 Oct 2018 12:18:49 -0300 Subject: [PATCH 083/105] Add returncode argument to pytest.exit If the argument is not None, it'll raise a SystemExit exception to cleanly exit pytest. --- src/_pytest/outcomes.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/_pytest/outcomes.py b/src/_pytest/outcomes.py index f6093ef76..a220b2cd1 100644 --- a/src/_pytest/outcomes.py +++ b/src/_pytest/outcomes.py @@ -57,10 +57,13 @@ class Exit(KeyboardInterrupt): # exposed helper methods -def exit(msg): +def exit(msg, returncode=None): """ exit testing process as if KeyboardInterrupt was triggered. """ __tracebackhide__ = True - raise Exit(msg) + if returncode: + raise SystemExit(returncode) + else: + raise Exit(msg) exit.Exception = Exit From ce55dcf64c31272ed8fff4c6f118af1956f3d0b3 Mon Sep 17 00:00:00 2001 From: Jose Carlos Menezes Date: Sun, 14 Oct 2018 12:20:10 -0300 Subject: [PATCH 084/105] Add test for calling pytest.exit with statuscode It checks that a SystemError was raised and the SystemError code is the same as the returncode argument. --- testing/test_runner.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/testing/test_runner.py b/testing/test_runner.py index b9538cfad..f9b7f8180 100644 --- a/testing/test_runner.py +++ b/testing/test_runner.py @@ -570,6 +570,15 @@ def test_pytest_exit_msg(testdir): result.stderr.fnmatch_lines(["Exit: oh noes"]) +def test_pytest_exit_returncode(): + try: + pytest.exit("hello", returncode=2) + except SystemExit as exc: + excinfo = _pytest._code.ExceptionInfo() + assert excinfo.errisinstance(SystemExit) + assert excinfo.value.code == 2 + + def test_pytest_fail_notrace_runtest(testdir): """Test pytest.fail(..., pytrace=False) does not show tracebacks during test run.""" testdir.makepyfile( From a0666354dd77b85919eee232ddd20a122bbff2d7 Mon Sep 17 00:00:00 2001 From: Jose Carlos Menezes Date: Sun, 14 Oct 2018 12:25:07 -0300 Subject: [PATCH 085/105] Update changelog --- changelog/4098.feature.rst | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog/4098.feature.rst diff --git a/changelog/4098.feature.rst b/changelog/4098.feature.rst new file mode 100644 index 000000000..d7a97ff62 --- /dev/null +++ b/changelog/4098.feature.rst @@ -0,0 +1 @@ +Add to pytest.exit a returncode argument to cleanly exit pytest. From 1dfa303b1e4656e3e87171e0ffc7db3dc36627a0 Mon Sep 17 00:00:00 2001 From: Ronny Pfannschmidt Date: Sun, 14 Oct 2018 21:20:34 +0200 Subject: [PATCH 086/105] fix #4135 - handle symlinks in tmp path cleanup --- changelog/4135.bugfix.rst | 1 + src/_pytest/pathlib.py | 2 ++ testing/test_tmpdir.py | 10 +++++++++- 3 files changed, 12 insertions(+), 1 deletion(-) create mode 100644 changelog/4135.bugfix.rst diff --git a/changelog/4135.bugfix.rst b/changelog/4135.bugfix.rst new file mode 100644 index 000000000..d4f8a851d --- /dev/null +++ b/changelog/4135.bugfix.rst @@ -0,0 +1 @@ +pathlib based tmpdir cleanup now correctly handles symlinks in the folder. diff --git a/src/_pytest/pathlib.py b/src/_pytest/pathlib.py index 439f4d9ba..cda5e9947 100644 --- a/src/_pytest/pathlib.py +++ b/src/_pytest/pathlib.py @@ -173,6 +173,8 @@ def delete_a_numbered_dir(path): def ensure_deletable(path, consider_lock_dead_if_created_before): """checks if a lock exists and breaks it if its considered dead""" + if path.is_symlink(): + return False lock = get_lock_path(path) if not lock.exists(): return True diff --git a/testing/test_tmpdir.py b/testing/test_tmpdir.py index 9244e309d..18b931c3f 100644 --- a/testing/test_tmpdir.py +++ b/testing/test_tmpdir.py @@ -232,7 +232,7 @@ class TestNumberedDir(object): assert not lock.exists() - def test_cleanup_keep(self, tmp_path): + def _do_cleanup(self, tmp_path): self.test_make(tmp_path) from _pytest.pathlib import cleanup_numbered_dir @@ -242,6 +242,9 @@ class TestNumberedDir(object): keep=2, consider_lock_dead_if_created_before=0, ) + + def test_cleanup_keep(self, tmp_path): + self._do_cleanup(tmp_path) a, b = tmp_path.iterdir() print(a, b) @@ -275,3 +278,8 @@ class TestNumberedDir(object): rmtree(adir, force=True) assert not adir.exists() + + def test_cleanup_symlink(self, tmp_path): + the_symlink = tmp_path / (self.PREFIX + "current") + the_symlink.symlink_to(tmp_path / (self.PREFIX + "5")) + self._do_cleanup(tmp_path) From 46d6a3fc27c41ad04b7658a6d454521635fbac9a Mon Sep 17 00:00:00 2001 From: Jose Carlos Menezes Date: Sun, 14 Oct 2018 16:36:53 -0300 Subject: [PATCH 087/105] Pass returncode to Error exception when creating instance --- src/_pytest/outcomes.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/src/_pytest/outcomes.py b/src/_pytest/outcomes.py index a220b2cd1..92b6824b6 100644 --- a/src/_pytest/outcomes.py +++ b/src/_pytest/outcomes.py @@ -49,8 +49,9 @@ class Failed(OutcomeException): class Exit(KeyboardInterrupt): """ raised for immediate program exits (no tracebacks/summaries)""" - def __init__(self, msg="unknown reason"): + def __init__(self, returncode=None, msg="unknown reason"): self.msg = msg + self.returncode = returncode KeyboardInterrupt.__init__(self, msg) @@ -60,10 +61,7 @@ class Exit(KeyboardInterrupt): def exit(msg, returncode=None): """ exit testing process as if KeyboardInterrupt was triggered. """ __tracebackhide__ = True - if returncode: - raise SystemExit(returncode) - else: - raise Exit(msg) + raise Exit(returncode, msg) exit.Exception = Exit From 836c9f82f12b8d2558d2b3a70edb6b53a6d2d5ac Mon Sep 17 00:00:00 2001 From: Jose Carlos Menezes Date: Sun, 14 Oct 2018 16:39:43 -0300 Subject: [PATCH 088/105] Set test session exitstatus value from Exit excetion return code --- src/_pytest/main.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/_pytest/main.py b/src/_pytest/main.py index 0171bda0c..c1896cdfa 100644 --- a/src/_pytest/main.py +++ b/src/_pytest/main.py @@ -185,10 +185,12 @@ def wrap_session(config, doit): session.exitstatus = EXIT_TESTSFAILED except KeyboardInterrupt: excinfo = _pytest._code.ExceptionInfo() - if initstate < 2 and isinstance(excinfo.value, exit.Exception): + exitstatus = EXIT_INTERRUPTED + if initstate <= 2 and isinstance(excinfo.value, exit.Exception): sys.stderr.write("{}: {}\n".format(excinfo.typename, excinfo.value.msg)) + exitstatus = excinfo.value.returncode config.hook.pytest_keyboard_interrupt(excinfo=excinfo) - session.exitstatus = EXIT_INTERRUPTED + session.exitstatus = exitstatus except: # noqa excinfo = _pytest._code.ExceptionInfo() config.notify_exception(excinfo, config.option) From 766d2daa06509839779cd2f33728535d2b428485 Mon Sep 17 00:00:00 2001 From: Jose Carlos Menezes Date: Sun, 14 Oct 2018 16:41:16 -0300 Subject: [PATCH 089/105] Update returncode exit test to check exitstatus returrned from test session --- testing/test_runner.py | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/testing/test_runner.py b/testing/test_runner.py index f9b7f8180..a3fffe81a 100644 --- a/testing/test_runner.py +++ b/testing/test_runner.py @@ -570,13 +570,16 @@ def test_pytest_exit_msg(testdir): result.stderr.fnmatch_lines(["Exit: oh noes"]) -def test_pytest_exit_returncode(): - try: - pytest.exit("hello", returncode=2) - except SystemExit as exc: - excinfo = _pytest._code.ExceptionInfo() - assert excinfo.errisinstance(SystemExit) - assert excinfo.value.code == 2 +def test_pytest_exit_returncode(testdir): + testdir.makepyfile( + """ + import pytest + def test_foo(): + pytest.exit("some exit msg", 99) + """ + ) + result = testdir.runpytest() + assert result.ret == 99 def test_pytest_fail_notrace_runtest(testdir): From d4351ac5a287eb96521f09618bbbe1c73f4e6884 Mon Sep 17 00:00:00 2001 From: Ronny Pfannschmidt Date: Sun, 14 Oct 2018 21:44:32 +0200 Subject: [PATCH 090/105] modernize packaging for setuptools>30.3 --- pyproject.toml | 3 +- setup.cfg | 30 +++++++++++++++++-- setup.py | 81 +++----------------------------------------------- 3 files changed, 34 insertions(+), 80 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index e82f051e1..c83bd853d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,7 @@ [build-system] requires = [ - "setuptools", + # sync with setup.py until we discard non-pep-517/518 + "setuptools>=30.3", "setuptools-scm", "wheel", ] diff --git a/setup.cfg b/setup.cfg index 816539e2e..e01ab74d5 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,3 +1,31 @@ +[metadata] + +name = pytest +description = pytest: simple powerful testing with Python +long_description = file: README.rst + + +license_file = LICENSE + +classifiers = + Development Status :: 6 - Mature + Intended Audience :: Developers + License :: OSI Approved :: MIT License + Operating System :: POSIX + Operating System :: Microsoft :: Windows + Operating System :: MacOS :: MacOS X + Topic :: Software Development :: Testing + Topic :: Software Development :: Libraries + Topic :: Utilities + Programming Language :: Python :: 2 + Programming Language :: Python :: 2.7 + Programming Language :: Python :: 3 + Programming Language :: Python :: 3.4 + Programming Language :: Python :: 3.5 + Programming Language :: Python :: 3.6 + Programming Language :: Python :: 3.7 + + [build_sphinx] source-dir = doc/en/ build-dir = doc/build @@ -13,8 +41,6 @@ universal = 1 ignore = _pytest/_version.py -[metadata] -license_file = LICENSE [devpi:upload] formats = sdist.tgz,bdist_wheel diff --git a/setup.py b/setup.py index 4c12fbfcc..83e2a36dc 100644 --- a/setup.py +++ b/setup.py @@ -1,63 +1,8 @@ import os -import sys -import setuptools -import pkg_resources from setuptools import setup -classifiers = [ - "Development Status :: 6 - Mature", - "Intended Audience :: Developers", - "License :: OSI Approved :: MIT License", - "Operating System :: POSIX", - "Operating System :: Microsoft :: Windows", - "Operating System :: MacOS :: MacOS X", - "Topic :: Software Development :: Testing", - "Topic :: Software Development :: Libraries", - "Topic :: Utilities", -] + [ - ("Programming Language :: Python :: %s" % x) - for x in "2 2.7 3 3.4 3.5 3.6 3.7".split() -] - -with open("README.rst") as fd: - long_description = fd.read() - - -def get_environment_marker_support_level(): - """ - Tests how well setuptools supports PEP-426 environment marker. - - The first known release to support it is 0.7 (and the earliest on PyPI seems to be 0.7.2 - so we're using that), see: https://setuptools.readthedocs.io/en/latest/history.html#id350 - - The support is later enhanced to allow direct conditional inclusions inside install_requires, - which is now recommended by setuptools. It first appeared in 36.2.0, went broken with 36.2.1, and - again worked since 36.2.2, so we're using that. See: - https://setuptools.readthedocs.io/en/latest/history.html#v36-2-2 - https://github.com/pypa/setuptools/issues/1099 - - References: - - * https://wheel.readthedocs.io/en/latest/index.html#defining-conditional-dependencies - * https://www.python.org/dev/peps/pep-0426/#environment-markers - * https://setuptools.readthedocs.io/en/latest/setuptools.html#declaring-platform-specific-dependencies - """ - try: - version = pkg_resources.parse_version(setuptools.__version__) - if version >= pkg_resources.parse_version("36.2.2"): - return 2 - if version >= pkg_resources.parse_version("0.7.2"): - return 1 - except Exception as exc: - sys.stderr.write("Could not test setuptool's version: %s\n" % exc) - - # as of testing on 2018-05-26 fedora was on version 37* and debian was on version 33+ - # we should consider erroring on those - return 0 - def main(): - extras_require = {} install_requires = [ "py>=1.5.0", # if py gets upgrade to >=1.6, remove _width_of_current_line in terminal.py "six>=1.10.0", @@ -65,32 +10,16 @@ def main(): "attrs>=17.4.0", "more-itertools>=4.0.0", "atomicwrites>=1.0", + 'funcsigs;python_version<"3.0"', + 'pathlib2>=2.2.0;python_version<"3.6"', + 'colorama;sys_platform=="win32"', ] # if _PYTEST_SETUP_SKIP_PLUGGY_DEP is set, skip installing pluggy; # used by tox.ini to test with pluggy master if "_PYTEST_SETUP_SKIP_PLUGGY_DEP" not in os.environ: install_requires.append("pluggy>=0.7") - environment_marker_support_level = get_environment_marker_support_level() - if environment_marker_support_level >= 2: - install_requires.append('funcsigs;python_version<"3.0"') - install_requires.append('pathlib2>=2.2.0;python_version<"3.6"') - install_requires.append('colorama;sys_platform=="win32"') - elif environment_marker_support_level == 1: - extras_require[':python_version<"3.0"'] = ["funcsigs"] - extras_require[':python_version<"3.6"'] = ["pathlib2>=2.2.0"] - extras_require[':sys_platform=="win32"'] = ["colorama"] - else: - if sys.platform == "win32": - install_requires.append("colorama") - if sys.version_info < (3, 0): - install_requires.append("funcsigs") - if sys.version_info < (3, 6): - install_requires.append("pathlib2>=2.2.0") setup( - name="pytest", - description="pytest: simple powerful testing with Python", - long_description=long_description, use_scm_version={"write_to": "src/_pytest/_version.py"}, url="https://docs.pytest.org/en/latest/", project_urls={ @@ -104,14 +33,12 @@ def main(): "Floris Bruynooghe, Brianna Laugher, Florian Bruhin and others" ), entry_points={"console_scripts": ["pytest=pytest:main", "py.test=pytest:main"]}, - classifiers=classifiers, keywords="test unittest", # the following should be enabled for release - setup_requires=["setuptools-scm"], + setup_requires=["setuptools-scm", "setuptools>30.3"], package_dir={"": "src"}, python_requires=">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*", install_requires=install_requires, - extras_require=extras_require, packages=[ "_pytest", "_pytest.assertion", From d4dfd526c11d610e3b2314a8d77ad990623b5db0 Mon Sep 17 00:00:00 2001 From: Jose Carlos Menezes Date: Sun, 14 Oct 2018 18:01:47 -0300 Subject: [PATCH 091/105] Update pytest.exit docstring --- src/_pytest/outcomes.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/_pytest/outcomes.py b/src/_pytest/outcomes.py index 92b6824b6..ca251c0e8 100644 --- a/src/_pytest/outcomes.py +++ b/src/_pytest/outcomes.py @@ -59,7 +59,11 @@ class Exit(KeyboardInterrupt): def exit(msg, returncode=None): - """ exit testing process as if KeyboardInterrupt was triggered. """ + """ + Exit testing process as if KeyboardInterrupt was triggered. + + :param int returncode: return code to be used when exiting pytest.. + """ __tracebackhide__ = True raise Exit(returncode, msg) From bbd1cbb0b3be15543b04674a8f358d2d1ff16a4a Mon Sep 17 00:00:00 2001 From: Jose Carlos Menezes Date: Sun, 14 Oct 2018 18:03:37 -0300 Subject: [PATCH 092/105] Update changelog to better reading --- changelog/4098.feature.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/changelog/4098.feature.rst b/changelog/4098.feature.rst index d7a97ff62..1a53de759 100644 --- a/changelog/4098.feature.rst +++ b/changelog/4098.feature.rst @@ -1 +1 @@ -Add to pytest.exit a returncode argument to cleanly exit pytest. +Add returncode argument to pytest.exit() to exit pytest with a specific return code. From ced62f30ba0d446518a138a4e20e9e9b7179c7cf Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Sun, 14 Oct 2018 18:21:04 -0300 Subject: [PATCH 093/105] Attempt to create symlinks even on Windows, skipping when not possible --- testing/test_tmpdir.py | 24 +++++++++++++++++------- 1 file changed, 17 insertions(+), 7 deletions(-) diff --git a/testing/test_tmpdir.py b/testing/test_tmpdir.py index 18b931c3f..9f4158eb7 100644 --- a/testing/test_tmpdir.py +++ b/testing/test_tmpdir.py @@ -1,7 +1,10 @@ from __future__ import absolute_import, division, print_function import sys -import py + +import six + import pytest +from _pytest.pathlib import Path def test_tmpdir_fixture(testdir): @@ -65,10 +68,6 @@ def test_basetemp(testdir): assert mytemp.join("hello").check() -@pytest.mark.skipif( - not hasattr(py.path.local, "mksymlinkto"), - reason="symlink not available on this platform", -) def test_tmpdir_always_is_realpath(testdir): # the reason why tmpdir should be a realpath is that # when you cd to it and do "os.getcwd()" you will anyway @@ -78,7 +77,7 @@ def test_tmpdir_always_is_realpath(testdir): # os.environ["PWD"] realtemp = testdir.tmpdir.mkdir("myrealtemp") linktemp = testdir.tmpdir.join("symlinktemp") - linktemp.mksymlinkto(realtemp) + attempt_symlink_to(linktemp, str(realtemp)) p = testdir.makepyfile( """ def test_1(tmpdir): @@ -281,5 +280,16 @@ class TestNumberedDir(object): def test_cleanup_symlink(self, tmp_path): the_symlink = tmp_path / (self.PREFIX + "current") - the_symlink.symlink_to(tmp_path / (self.PREFIX + "5")) + attempt_symlink_to(the_symlink, tmp_path / (self.PREFIX + "5")) self._do_cleanup(tmp_path) + + +def attempt_symlink_to(path, to_path): + """Try to make a symlink from "path" to "to_path", skipping in case this platform + does not support it or we don't have sufficient privileges (common on Windows).""" + if sys.platform.startswith("win") and six.PY2: + pytest.skip("pathlib for some reason cannot make symlinks on Python 2") + try: + Path(path).symlink_to(Path(to_path)) + except OSError: + pytest.skip("could not create symbolic link") From d32f2c5c142b71b9fca55860c4d00652ab7a9aa5 Mon Sep 17 00:00:00 2001 From: Jose Carlos Menezes Date: Sun, 14 Oct 2018 18:42:55 -0300 Subject: [PATCH 094/105] Change Exit.__init__ params order to keep backward compatibility --- src/_pytest/outcomes.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/_pytest/outcomes.py b/src/_pytest/outcomes.py index ca251c0e8..d12e78266 100644 --- a/src/_pytest/outcomes.py +++ b/src/_pytest/outcomes.py @@ -49,7 +49,7 @@ class Failed(OutcomeException): class Exit(KeyboardInterrupt): """ raised for immediate program exits (no tracebacks/summaries)""" - def __init__(self, returncode=None, msg="unknown reason"): + def __init__(self, msg="unknown reason", returncode=None): self.msg = msg self.returncode = returncode KeyboardInterrupt.__init__(self, msg) From 76fb9970c8cc561e1ed6ee21fc10f46c60fc8637 Mon Sep 17 00:00:00 2001 From: Jose Carlos Menezes Date: Sun, 14 Oct 2018 18:43:48 -0300 Subject: [PATCH 095/105] Check if returncode is not None before assigning test return code --- src/_pytest/main.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/_pytest/main.py b/src/_pytest/main.py index c1896cdfa..2d6cea668 100644 --- a/src/_pytest/main.py +++ b/src/_pytest/main.py @@ -188,7 +188,8 @@ def wrap_session(config, doit): exitstatus = EXIT_INTERRUPTED if initstate <= 2 and isinstance(excinfo.value, exit.Exception): sys.stderr.write("{}: {}\n".format(excinfo.typename, excinfo.value.msg)) - exitstatus = excinfo.value.returncode + if excinfo.value.returncode is not None: + exitstatus = excinfo.value.returncode config.hook.pytest_keyboard_interrupt(excinfo=excinfo) session.exitstatus = exitstatus except: # noqa From 40091ec2c733331b45ec7ee68538aab6afbcdec1 Mon Sep 17 00:00:00 2001 From: Jose Carlos Menezes Date: Sun, 14 Oct 2018 18:44:53 -0300 Subject: [PATCH 096/105] Update pytest.exit docstring --- src/_pytest/outcomes.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/_pytest/outcomes.py b/src/_pytest/outcomes.py index d12e78266..35b3bfb9f 100644 --- a/src/_pytest/outcomes.py +++ b/src/_pytest/outcomes.py @@ -62,7 +62,8 @@ def exit(msg, returncode=None): """ Exit testing process as if KeyboardInterrupt was triggered. - :param int returncode: return code to be used when exiting pytest.. + :param str msg: message to display upon exit. + :param int returncode: return code to be used when exiting pytest. """ __tracebackhide__ = True raise Exit(returncode, msg) From 27d932e882152c828352ec4855ac2e16d23cc115 Mon Sep 17 00:00:00 2001 From: Jose Carlos Menezes Date: Sun, 14 Oct 2018 18:48:32 -0300 Subject: [PATCH 097/105] Fix order of parameters when raising Exit exception --- src/_pytest/outcomes.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/_pytest/outcomes.py b/src/_pytest/outcomes.py index 35b3bfb9f..4c7958384 100644 --- a/src/_pytest/outcomes.py +++ b/src/_pytest/outcomes.py @@ -66,7 +66,7 @@ def exit(msg, returncode=None): :param int returncode: return code to be used when exiting pytest. """ __tracebackhide__ = True - raise Exit(returncode, msg) + raise Exit(msg, returncode) exit.Exception = Exit From 7855284ef72bb7176aca7eba90e02f6825eb17f9 Mon Sep 17 00:00:00 2001 From: Ronny Pfannschmidt Date: Mon, 15 Oct 2018 07:30:07 +0200 Subject: [PATCH 098/105] move most setuptools parameters over to setup.cfg --- setup.cfg | 26 +++++++++++++++++++++- setup.py | 65 ++++++++++++++++++++----------------------------------- 2 files changed, 48 insertions(+), 43 deletions(-) diff --git a/setup.cfg b/setup.cfg index e01ab74d5..a898b66c6 100644 --- a/setup.cfg +++ b/setup.cfg @@ -3,10 +3,16 @@ name = pytest description = pytest: simple powerful testing with Python long_description = file: README.rst +url = "https://docs.pytest.org/en/latest/" +project_urls = + Source=https://github.com/pytest-dev/pytest + Tracker=https://github.com/pytest-dev/pytest/issues +author = Holger Krekel, Bruno Oliveira, Ronny Pfannschmidt, Floris Bruynooghe, Brianna Laugher, Florian Bruhin and others +license = MIT license license_file = LICENSE - +keywords = test, unittest classifiers = Development Status :: 6 - Mature Intended Audience :: Developers @@ -24,7 +30,25 @@ classifiers = Programming Language :: Python :: 3.5 Programming Language :: Python :: 3.6 Programming Language :: Python :: 3.7 +platforms = unix, linux, osx, cygwin, win32 +[options] +zip_safe = no +packages = + _pytest + _pytest.assertion + _pytest._code + _pytest.mark + _pytest.config + +py_modules = pytest +python_requires = >=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.* + + +[options.entry_points] +console_scripts = + pytest=pytest:main + py.test=pytest:main [build_sphinx] source-dir = doc/en/ diff --git a/setup.py b/setup.py index 83e2a36dc..6bab5312d 100644 --- a/setup.py +++ b/setup.py @@ -2,52 +2,33 @@ import os from setuptools import setup -def main(): - install_requires = [ - "py>=1.5.0", # if py gets upgrade to >=1.6, remove _width_of_current_line in terminal.py - "six>=1.10.0", - "setuptools", - "attrs>=17.4.0", - "more-itertools>=4.0.0", - "atomicwrites>=1.0", - 'funcsigs;python_version<"3.0"', - 'pathlib2>=2.2.0;python_version<"3.6"', - 'colorama;sys_platform=="win32"', - ] - # if _PYTEST_SETUP_SKIP_PLUGGY_DEP is set, skip installing pluggy; - # used by tox.ini to test with pluggy master - if "_PYTEST_SETUP_SKIP_PLUGGY_DEP" not in os.environ: - install_requires.append("pluggy>=0.7") +# TODO: if py gets upgrade to >=1.6, +# remove _width_of_current_line in terminal.py +INSTALL_REQUIRES = [ + "py>=1.5.0", + "six>=1.10.0", + "setuptools", + "attrs>=17.4.0", + "more-itertools>=4.0.0", + "atomicwrites>=1.0", + 'funcsigs;python_version<"3.0"', + 'pathlib2>=2.2.0;python_version<"3.6"', + 'colorama;sys_platform=="win32"', +] + +# if _PYTEST_SETUP_SKIP_PLUGGY_DEP is set, skip installing pluggy; +# used by tox.ini to test with pluggy master +if "_PYTEST_SETUP_SKIP_PLUGGY_DEP" not in os.environ: + INSTALL_REQUIRES.append("pluggy>=0.7") + + +def main(): setup( use_scm_version={"write_to": "src/_pytest/_version.py"}, - url="https://docs.pytest.org/en/latest/", - project_urls={ - "Source": "https://github.com/pytest-dev/pytest", - "Tracker": "https://github.com/pytest-dev/pytest/issues", - }, - license="MIT license", - platforms=["unix", "linux", "osx", "cygwin", "win32"], - author=( - "Holger Krekel, Bruno Oliveira, Ronny Pfannschmidt, " - "Floris Bruynooghe, Brianna Laugher, Florian Bruhin and others" - ), - entry_points={"console_scripts": ["pytest=pytest:main", "py.test=pytest:main"]}, - keywords="test unittest", - # the following should be enabled for release - setup_requires=["setuptools-scm", "setuptools>30.3"], + setup_requires=["setuptools-scm", "setuptools>=30.3"], package_dir={"": "src"}, - python_requires=">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*", - install_requires=install_requires, - packages=[ - "_pytest", - "_pytest.assertion", - "_pytest._code", - "_pytest.mark", - "_pytest.config", - ], - py_modules=["pytest"], - zip_safe=False, + install_requires=INSTALL_REQUIRES, ) From d65c7658d507597e7fb42590290c8cd2feeef639 Mon Sep 17 00:00:00 2001 From: Ronny Pfannschmidt Date: Mon, 15 Oct 2018 07:32:38 +0200 Subject: [PATCH 099/105] changelog --- changelog/4149.feature.rst | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog/4149.feature.rst diff --git a/changelog/4149.feature.rst b/changelog/4149.feature.rst new file mode 100644 index 000000000..7f9908b15 --- /dev/null +++ b/changelog/4149.feature.rst @@ -0,0 +1 @@ +Require setuptools>=30.3 and move most of the metadata to ``setup.cfg``. From e8c220b9bd31924aa94f0410d3a6f0c50e9ab18d Mon Sep 17 00:00:00 2001 From: Thomas Hisch Date: Mon, 15 Oct 2018 19:29:39 +0200 Subject: [PATCH 100/105] Increase required verbosity level for debug output To show the subclassed file in legacy test suits in the runtest output you have to set the verbosity level to at least "-vv" now. Closes #3211 --- changelog/4159.feature.rst | 3 +++ src/_pytest/terminal.py | 4 +++- testing/acceptance_test.py | 4 ++-- testing/test_terminal.py | 4 ++-- 4 files changed, 10 insertions(+), 5 deletions(-) create mode 100644 changelog/4159.feature.rst diff --git a/changelog/4159.feature.rst b/changelog/4159.feature.rst new file mode 100644 index 000000000..28ca7462e --- /dev/null +++ b/changelog/4159.feature.rst @@ -0,0 +1,3 @@ +For test-suites containing test classes, the information about the subclassed +module is now output only if a higher verbosity level is specified (at least +"-vv"). diff --git a/src/_pytest/terminal.py b/src/_pytest/terminal.py index f4dbbe61a..8deb330cc 100644 --- a/src/_pytest/terminal.py +++ b/src/_pytest/terminal.py @@ -676,7 +676,9 @@ class TerminalReporter(object): if fspath: res = mkrel(nodeid).replace("::()", "") # parens-normalization - if nodeid.split("::")[0] != fspath.replace("\\", nodes.SEP): + if self.verbosity >= 2 and nodeid.split("::")[0] != fspath.replace( + "\\", nodes.SEP + ): res += " <- " + self.startdir.bestrelpath(fspath) else: res = "[location]" diff --git a/testing/acceptance_test.py b/testing/acceptance_test.py index 2a6c75740..3dd56bf87 100644 --- a/testing/acceptance_test.py +++ b/testing/acceptance_test.py @@ -760,8 +760,8 @@ class TestInvocationVariants(object): if hasattr(py.path.local, "mksymlinkto"): result.stdout.fnmatch_lines( [ - "lib/foo/bar/test_bar.py::test_bar <- local/lib/foo/bar/test_bar.py PASSED*", - "lib/foo/bar/test_bar.py::test_other <- local/lib/foo/bar/test_bar.py PASSED*", + "lib/foo/bar/test_bar.py::test_bar PASSED*", + "lib/foo/bar/test_bar.py::test_other PASSED*", "*2 passed*", ] ) diff --git a/testing/test_terminal.py b/testing/test_terminal.py index 7651f3ab3..af2dc2f00 100644 --- a/testing/test_terminal.py +++ b/testing/test_terminal.py @@ -154,7 +154,7 @@ class TestTerminal(object): ) result = testdir.runpytest(p2) result.stdout.fnmatch_lines(["*test_p2.py .*", "*1 passed*"]) - result = testdir.runpytest("-v", p2) + result = testdir.runpytest("-vv", p2) result.stdout.fnmatch_lines( ["*test_p2.py::TestMore::test_p1* <- *test_p1.py*PASSED*"] ) @@ -170,7 +170,7 @@ class TestTerminal(object): """ ) ) - result = testdir.runpytest("-v") + result = testdir.runpytest("-vv") assert result.ret == 0 result.stdout.fnmatch_lines(["*a123/test_hello123.py*PASS*"]) assert " <- " not in result.stdout.str() From 39a13d70647ae8c7a984209a3424b5b4052efa4f Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Mon, 15 Oct 2018 20:17:02 +0000 Subject: [PATCH 101/105] Fix tmp_path example in docs --- doc/en/tmpdir.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/en/tmpdir.rst b/doc/en/tmpdir.rst index 728621152..aff9a9308 100644 --- a/doc/en/tmpdir.rst +++ b/doc/en/tmpdir.rst @@ -31,7 +31,7 @@ created in the `base temporary directory`_. p = d / "hello.txt" p.write_text(CONTENT) assert p.read_text() == CONTENT - assert len(tmpdir.listdir()) == 1 + assert len(list(tmp_path.iterdir())) == 1 assert 0 Running this would result in a passed test except for the last From 2868c314953e3a2cf2c753ebb87710d65c906cfa Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Mon, 15 Oct 2018 20:23:30 +0000 Subject: [PATCH 102/105] Preparing release version 3.9.0 --- CHANGELOG.rst | 146 ++++++++++++++++++++++++++++++ changelog/2293.feature.rst | 4 - changelog/2293.trivial.rst | 1 - changelog/2535.bugfix.rst | 1 - changelog/3057.bugfix.rst | 1 - changelog/3332.feature.rst | 4 - changelog/3616.deprecation.rst | 22 ----- changelog/3713.doc.rst | 1 - changelog/3849.feature.rst | 1 - changelog/3946.bugfix.rst | 2 - changelog/3964.feature.rst | 2 - changelog/3985.feature.rst | 1 - changelog/3988.deprecation.rst | 1 - changelog/3988.trivial.rst | 1 - changelog/4013.feature.rst | 2 - changelog/4058.doc.rst | 1 - changelog/4063.trivial.rst | 1 - changelog/4064.doc.rst | 1 - changelog/4066.bugfix.rst | 1 - changelog/4073.feature.rst | 1 - changelog/4093.trivial.rst | 1 - changelog/4098.feature.rst | 1 - changelog/4102.bugfix.rst | 1 - changelog/4102.feature.rst | 4 - changelog/4108.bugfix.rst | 5 - changelog/4132.bugfix.rst | 1 - changelog/4135.bugfix.rst | 1 - changelog/4149.feature.rst | 1 - changelog/4151.doc.rst | 1 - changelog/4152.bugfix.rst | 1 - doc/en/announce/index.rst | 1 + doc/en/announce/release-3.9.0.rst | 43 +++++++++ doc/en/builtin.rst | 14 ++- doc/en/example/markers.rst | 16 ++-- doc/en/example/nonpython.rst | 2 +- doc/en/example/simple.rst | 4 +- doc/en/fixture.rst | 6 +- doc/en/tmpdir.rst | 23 ++++- doc/en/writing_plugins.rst | 14 ++- 39 files changed, 252 insertions(+), 83 deletions(-) delete mode 100644 changelog/2293.feature.rst delete mode 100644 changelog/2293.trivial.rst delete mode 100644 changelog/2535.bugfix.rst delete mode 100644 changelog/3057.bugfix.rst delete mode 100644 changelog/3332.feature.rst delete mode 100644 changelog/3616.deprecation.rst delete mode 100644 changelog/3713.doc.rst delete mode 100644 changelog/3849.feature.rst delete mode 100644 changelog/3946.bugfix.rst delete mode 100644 changelog/3964.feature.rst delete mode 100644 changelog/3985.feature.rst delete mode 100644 changelog/3988.deprecation.rst delete mode 100644 changelog/3988.trivial.rst delete mode 100644 changelog/4013.feature.rst delete mode 100644 changelog/4058.doc.rst delete mode 100644 changelog/4063.trivial.rst delete mode 100644 changelog/4064.doc.rst delete mode 100644 changelog/4066.bugfix.rst delete mode 100644 changelog/4073.feature.rst delete mode 100644 changelog/4093.trivial.rst delete mode 100644 changelog/4098.feature.rst delete mode 100644 changelog/4102.bugfix.rst delete mode 100644 changelog/4102.feature.rst delete mode 100644 changelog/4108.bugfix.rst delete mode 100644 changelog/4132.bugfix.rst delete mode 100644 changelog/4135.bugfix.rst delete mode 100644 changelog/4149.feature.rst delete mode 100644 changelog/4151.doc.rst delete mode 100644 changelog/4152.bugfix.rst create mode 100644 doc/en/announce/release-3.9.0.rst diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 26bc28af1..0f0193864 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -18,6 +18,152 @@ with advance notice in the **Deprecations** section of releases. .. towncrier release notes start +pytest 3.9.0 (2018-10-15) +========================= + +Deprecations +------------ + +- `#3616 `_: The following accesses have been documented as deprecated for years, but are now actually emitting deprecation warnings. + + * Access of ``Module``, ``Function``, ``Class``, ``Instance``, ``File`` and ``Item`` through ``Node`` instances. Now + users will this warning:: + + usage of Function.Module is deprecated, please use pytest.Module instead + + Users should just ``import pytest`` and access those objects using the ``pytest`` module. + + * ``request.cached_setup``, this was the precursor of the setup/teardown mechanism available to fixtures. You can + consult `funcarg comparision section in the docs `_. + + * Using objects named ``"Class"`` as a way to customize the type of nodes that are collected in ``Collector`` + subclasses has been deprecated. Users instead should use ``pytest_collect_make_item`` to customize node types during + collection. + + This issue should affect only advanced plugins who create new collection types, so if you see this warning + message please contact the authors so they can change the code. + + * The warning that produces the message below has changed to ``RemovedInPytest4Warning``:: + + getfuncargvalue is deprecated, use getfixturevalue + + +- `#3988 `_: Add a Deprecation warning for pytest.ensuretemp as it was deprecated since a while. + + + +Features +-------- + +- `#2293 `_: Improve usage errors messages by hiding internal details which can be distracting and noisy. + + This has the side effect that some error conditions that previously raised generic errors (such as + ``ValueError`` for unregistered marks) are now raising ``Failed`` exceptions. + + +- `#3332 `_: Improve the error displayed when a ``conftest.py`` file could not be imported. + + In order to implement this, a new ``chain`` parameter was added to ``ExceptionInfo.getrepr`` + to show or hide chained tracebacks in Python 3 (defaults to ``True``). + + +- `#3849 `_: Add ``empty_parameter_set_mark=fail_at_collect`` ini option for raising an exception when parametrize collects an empty set. + + +- `#3964 `_: Log messages generated in the collection phase are shown when + live-logging is enabled and/or when they are logged to a file. + + +- `#3985 `_: Introduce ``tmp_path`` as a fixture providing a Path object. + + +- `#4013 `_: Deprecation warnings are now shown even if you customize the warnings filters yourself. In the previous version + any customization would override pytest's filters and deprecation warnings would fall back to being hidden by default. + + +- `#4073 `_: Allow specification of timeout for ``Testdir.runpytest_subprocess()`` and ``Testdir.run()``. + + +- `#4098 `_: Add returncode argument to pytest.exit() to exit pytest with a specific return code. + + +- `#4102 `_: Reimplement ``pytest.deprecated_call`` using ``pytest.warns`` so it supports the ``match='...'`` keyword argument. + + This has the side effect that ``pytest.deprecated_call`` now raises ``pytest.fail.Exception`` instead + of ``AssertionError``. + + +- `#4149 `_: Require setuptools>=30.3 and move most of the metadata to ``setup.cfg``. + + + +Bug Fixes +--------- + +- `#2535 `_: Improve error message when test functions of ``unittest.TestCase`` subclasses use a parametrized fixture. + + +- `#3057 `_: ``request.fixturenames`` now correctly returns the name of fixtures created by ``request.getfixturevalue()``. + + +- `#3946 `_: Warning filters passed as command line options using ``-W`` now take precedence over filters defined in ``ini`` + configuration files. + + +- `#4066 `_: Fix source reindenting by using ``textwrap.dedent`` directly. + + +- `#4102 `_: ``pytest.warn`` will capture previously-warned warnings in Python 2. Previously they were never raised. + + +- `#4108 `_: Resolve symbolic links for args. + + This fixes running ``pytest tests/test_foo.py::test_bar``, where ``tests`` + is a symlink to ``project/app/tests``: + previously ``project/app/conftest.py`` would be ignored for fixtures then. + + +- `#4132 `_: Fix duplicate printing of internal errors when using ``--pdb``. + + +- `#4135 `_: pathlib based tmpdir cleanup now correctly handles symlinks in the folder. + + +- `#4152 `_: Display the filename when encountering ``SyntaxWarning``. + + + +Improved Documentation +---------------------- + +- `#3713 `_: Update usefixtures documentation to clarify that it can't be used with fixture functions. + + +- `#4058 `_: Update fixture documentation to specify that a fixture can be invoked twice in the scope it's defined for. + + +- `#4064 `_: According to unittest.rst, setUpModule and tearDownModule were not implemented, but it turns out they are. So updated the documentation for unittest. + + +- `#4151 `_: Add tempir testing example to CONTRIBUTING.rst guide + + + +Trivial/Internal Changes +------------------------ + +- `#2293 `_: The internal ``MarkerError`` exception has been removed. + + +- `#3988 `_: Port the implementation of tmpdir to pathlib. + + +- `#4063 `_: Exclude 0.00 second entries from ``--duration`` output unless ``-vv`` is passed on the command-line. + + +- `#4093 `_: Fixed formatting of string literals in internal tests. + + pytest 3.8.2 (2018-10-02) ========================= diff --git a/changelog/2293.feature.rst b/changelog/2293.feature.rst deleted file mode 100644 index 5e56ba321..000000000 --- a/changelog/2293.feature.rst +++ /dev/null @@ -1,4 +0,0 @@ -Improve usage errors messages by hiding internal details which can be distracting and noisy. - -This has the side effect that some error conditions that previously raised generic errors (such as -``ValueError`` for unregistered marks) are now raising ``Failed`` exceptions. diff --git a/changelog/2293.trivial.rst b/changelog/2293.trivial.rst deleted file mode 100644 index a11245127..000000000 --- a/changelog/2293.trivial.rst +++ /dev/null @@ -1 +0,0 @@ -The internal ``MarkerError`` exception has been removed. diff --git a/changelog/2535.bugfix.rst b/changelog/2535.bugfix.rst deleted file mode 100644 index ec16e81ea..000000000 --- a/changelog/2535.bugfix.rst +++ /dev/null @@ -1 +0,0 @@ -Improve error message when test functions of ``unittest.TestCase`` subclasses use a parametrized fixture. diff --git a/changelog/3057.bugfix.rst b/changelog/3057.bugfix.rst deleted file mode 100644 index 8cc22f278..000000000 --- a/changelog/3057.bugfix.rst +++ /dev/null @@ -1 +0,0 @@ -``request.fixturenames`` now correctly returns the name of fixtures created by ``request.getfixturevalue()``. diff --git a/changelog/3332.feature.rst b/changelog/3332.feature.rst deleted file mode 100644 index e0110c451..000000000 --- a/changelog/3332.feature.rst +++ /dev/null @@ -1,4 +0,0 @@ -Improve the error displayed when a ``conftest.py`` file could not be imported. - -In order to implement this, a new ``chain`` parameter was added to ``ExceptionInfo.getrepr`` -to show or hide chained tracebacks in Python 3 (defaults to ``True``). diff --git a/changelog/3616.deprecation.rst b/changelog/3616.deprecation.rst deleted file mode 100644 index 8ea1b4d3d..000000000 --- a/changelog/3616.deprecation.rst +++ /dev/null @@ -1,22 +0,0 @@ -The following accesses have been documented as deprecated for years, but are now actually emitting deprecation warnings. - -* Access of ``Module``, ``Function``, ``Class``, ``Instance``, ``File`` and ``Item`` through ``Node`` instances. Now - users will this warning:: - - usage of Function.Module is deprecated, please use pytest.Module instead - - Users should just ``import pytest`` and access those objects using the ``pytest`` module. - -* ``request.cached_setup``, this was the precursor of the setup/teardown mechanism available to fixtures. You can - consult `funcarg comparision section in the docs `_. - -* Using objects named ``"Class"`` as a way to customize the type of nodes that are collected in ``Collector`` - subclasses has been deprecated. Users instead should use ``pytest_collect_make_item`` to customize node types during - collection. - - This issue should affect only advanced plugins who create new collection types, so if you see this warning - message please contact the authors so they can change the code. - -* The warning that produces the message below has changed to ``RemovedInPytest4Warning``:: - - getfuncargvalue is deprecated, use getfixturevalue diff --git a/changelog/3713.doc.rst b/changelog/3713.doc.rst deleted file mode 100644 index c1d6b51b8..000000000 --- a/changelog/3713.doc.rst +++ /dev/null @@ -1 +0,0 @@ -Update usefixtures documentation to clarify that it can't be used with fixture functions. diff --git a/changelog/3849.feature.rst b/changelog/3849.feature.rst deleted file mode 100644 index 26cbfe7b4..000000000 --- a/changelog/3849.feature.rst +++ /dev/null @@ -1 +0,0 @@ -Add ``empty_parameter_set_mark=fail_at_collect`` ini option for raising an exception when parametrize collects an empty set. diff --git a/changelog/3946.bugfix.rst b/changelog/3946.bugfix.rst deleted file mode 100644 index 1b5248b4c..000000000 --- a/changelog/3946.bugfix.rst +++ /dev/null @@ -1,2 +0,0 @@ -Warning filters passed as command line options using ``-W`` now take precedence over filters defined in ``ini`` -configuration files. diff --git a/changelog/3964.feature.rst b/changelog/3964.feature.rst deleted file mode 100644 index 37788c6f7..000000000 --- a/changelog/3964.feature.rst +++ /dev/null @@ -1,2 +0,0 @@ -Log messages generated in the collection phase are shown when -live-logging is enabled and/or when they are logged to a file. diff --git a/changelog/3985.feature.rst b/changelog/3985.feature.rst deleted file mode 100644 index 19070cad0..000000000 --- a/changelog/3985.feature.rst +++ /dev/null @@ -1 +0,0 @@ -Introduce ``tmp_path`` as a fixture providing a Path object. diff --git a/changelog/3988.deprecation.rst b/changelog/3988.deprecation.rst deleted file mode 100644 index b731112e4..000000000 --- a/changelog/3988.deprecation.rst +++ /dev/null @@ -1 +0,0 @@ -Add a Deprecation warning for pytest.ensuretemp as it was deprecated since a while. diff --git a/changelog/3988.trivial.rst b/changelog/3988.trivial.rst deleted file mode 100644 index 876db9798..000000000 --- a/changelog/3988.trivial.rst +++ /dev/null @@ -1 +0,0 @@ -Port the implementation of tmpdir to pathlib. diff --git a/changelog/4013.feature.rst b/changelog/4013.feature.rst deleted file mode 100644 index 84c3ab79d..000000000 --- a/changelog/4013.feature.rst +++ /dev/null @@ -1,2 +0,0 @@ -Deprecation warnings are now shown even if you customize the warnings filters yourself. In the previous version -any customization would override pytest's filters and deprecation warnings would fall back to being hidden by default. diff --git a/changelog/4058.doc.rst b/changelog/4058.doc.rst deleted file mode 100644 index 51d568f54..000000000 --- a/changelog/4058.doc.rst +++ /dev/null @@ -1 +0,0 @@ -Update fixture documentation to specify that a fixture can be invoked twice in the scope it's defined for. diff --git a/changelog/4063.trivial.rst b/changelog/4063.trivial.rst deleted file mode 100644 index 465356401..000000000 --- a/changelog/4063.trivial.rst +++ /dev/null @@ -1 +0,0 @@ -Exclude 0.00 second entries from ``--duration`` output unless ``-vv`` is passed on the command-line. diff --git a/changelog/4064.doc.rst b/changelog/4064.doc.rst deleted file mode 100644 index 7b34fe43e..000000000 --- a/changelog/4064.doc.rst +++ /dev/null @@ -1 +0,0 @@ -According to unittest.rst, setUpModule and tearDownModule were not implemented, but it turns out they are. So updated the documentation for unittest. diff --git a/changelog/4066.bugfix.rst b/changelog/4066.bugfix.rst deleted file mode 100644 index 64980d6e8..000000000 --- a/changelog/4066.bugfix.rst +++ /dev/null @@ -1 +0,0 @@ -Fix source reindenting by using ``textwrap.dedent`` directly. diff --git a/changelog/4073.feature.rst b/changelog/4073.feature.rst deleted file mode 100644 index 5b0ed5927..000000000 --- a/changelog/4073.feature.rst +++ /dev/null @@ -1 +0,0 @@ -Allow specification of timeout for ``Testdir.runpytest_subprocess()`` and ``Testdir.run()``. diff --git a/changelog/4093.trivial.rst b/changelog/4093.trivial.rst deleted file mode 100644 index cbfbeb00d..000000000 --- a/changelog/4093.trivial.rst +++ /dev/null @@ -1 +0,0 @@ -Fixed formatting of string literals in internal tests. diff --git a/changelog/4098.feature.rst b/changelog/4098.feature.rst deleted file mode 100644 index 1a53de759..000000000 --- a/changelog/4098.feature.rst +++ /dev/null @@ -1 +0,0 @@ -Add returncode argument to pytest.exit() to exit pytest with a specific return code. diff --git a/changelog/4102.bugfix.rst b/changelog/4102.bugfix.rst deleted file mode 100644 index dd066c38d..000000000 --- a/changelog/4102.bugfix.rst +++ /dev/null @@ -1 +0,0 @@ -``pytest.warn`` will capture previously-warned warnings in Python 2. Previously they were never raised. diff --git a/changelog/4102.feature.rst b/changelog/4102.feature.rst deleted file mode 100644 index ee43ddc24..000000000 --- a/changelog/4102.feature.rst +++ /dev/null @@ -1,4 +0,0 @@ -Reimplement ``pytest.deprecated_call`` using ``pytest.warns`` so it supports the ``match='...'`` keyword argument. - -This has the side effect that ``pytest.deprecated_call`` now raises ``pytest.fail.Exception`` instead -of ``AssertionError``. diff --git a/changelog/4108.bugfix.rst b/changelog/4108.bugfix.rst deleted file mode 100644 index d136d5996..000000000 --- a/changelog/4108.bugfix.rst +++ /dev/null @@ -1,5 +0,0 @@ -Resolve symbolic links for args. - -This fixes running ``pytest tests/test_foo.py::test_bar``, where ``tests`` -is a symlink to ``project/app/tests``: -previously ``project/app/conftest.py`` would be ignored for fixtures then. diff --git a/changelog/4132.bugfix.rst b/changelog/4132.bugfix.rst deleted file mode 100644 index 1fbb9afad..000000000 --- a/changelog/4132.bugfix.rst +++ /dev/null @@ -1 +0,0 @@ -Fix duplicate printing of internal errors when using ``--pdb``. diff --git a/changelog/4135.bugfix.rst b/changelog/4135.bugfix.rst deleted file mode 100644 index d4f8a851d..000000000 --- a/changelog/4135.bugfix.rst +++ /dev/null @@ -1 +0,0 @@ -pathlib based tmpdir cleanup now correctly handles symlinks in the folder. diff --git a/changelog/4149.feature.rst b/changelog/4149.feature.rst deleted file mode 100644 index 7f9908b15..000000000 --- a/changelog/4149.feature.rst +++ /dev/null @@ -1 +0,0 @@ -Require setuptools>=30.3 and move most of the metadata to ``setup.cfg``. diff --git a/changelog/4151.doc.rst b/changelog/4151.doc.rst deleted file mode 100644 index da561002a..000000000 --- a/changelog/4151.doc.rst +++ /dev/null @@ -1 +0,0 @@ -Add tempir testing example to CONTRIBUTING.rst guide diff --git a/changelog/4152.bugfix.rst b/changelog/4152.bugfix.rst deleted file mode 100644 index ce2547294..000000000 --- a/changelog/4152.bugfix.rst +++ /dev/null @@ -1 +0,0 @@ -Display the filename when encountering ``SyntaxWarning``. diff --git a/doc/en/announce/index.rst b/doc/en/announce/index.rst index e6c712120..4dad6fd4f 100644 --- a/doc/en/announce/index.rst +++ b/doc/en/announce/index.rst @@ -6,6 +6,7 @@ Release announcements :maxdepth: 2 + release-3.9.0 release-3.8.2 release-3.8.1 release-3.8.0 diff --git a/doc/en/announce/release-3.9.0.rst b/doc/en/announce/release-3.9.0.rst new file mode 100644 index 000000000..14cfbe903 --- /dev/null +++ b/doc/en/announce/release-3.9.0.rst @@ -0,0 +1,43 @@ +pytest-3.9.0 +======================================= + +The pytest team is proud to announce the 3.9.0 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 +to take a look at the CHANGELOG: + + https://docs.pytest.org/en/latest/changelog.html + +For complete documentation, please visit: + + https://docs.pytest.org/en/latest/ + +As usual, you can upgrade from pypi via: + + pip install -U pytest + +Thanks to all who contributed to this release, among them: + +* Andrea Cimatoribus +* Ankit Goel +* Anthony Sottile +* Ben Eyal +* Bruno Oliveira +* Daniel Hahler +* Jeffrey Rackauckas +* Jose Carlos Menezes +* Kyle Altendorf +* Niklas JQ +* Palash Chatterjee +* Ronny Pfannschmidt +* Thomas Hess +* Thomas Hisch +* Tomer Keren +* Victor Maryama + + +Happy testing, +The Pytest Development Team diff --git a/doc/en/builtin.rst b/doc/en/builtin.rst index e52151a1b..f921b5d64 100644 --- a/doc/en/builtin.rst +++ b/doc/en/builtin.rst @@ -104,7 +104,9 @@ For information about fixtures, see :ref:`fixtures`. To see a complete list of a See http://docs.python.org/library/warnings.html for information on warning categories. tmpdir_factory - Return a TempdirFactory instance for the test session. + Return a :class:`_pytest.tmpdir.TempdirFactory` instance for the test session. + tmp_path_factory + Return a :class:`_pytest.tmpdir.TempPathFactory` instance for the test session. tmpdir Return a temporary directory path object which is unique to each test function invocation, @@ -113,6 +115,16 @@ For information about fixtures, see :ref:`fixtures`. To see a complete list of a path object. .. _`py.path.local`: https://py.readthedocs.io/en/latest/path.html + tmp_path + Return a temporary directory path object + which is unique to each test function invocation, + created as a sub directory of the base temporary + directory. The returned object is a :class:`pathlib.Path` + object. + + .. note:: + + in python < 3.6 this is a pathlib2.Path no tests ran in 0.12 seconds diff --git a/doc/en/example/markers.rst b/doc/en/example/markers.rst index a2cb8a676..cb6368a64 100644 --- a/doc/en/example/markers.rst +++ b/doc/en/example/markers.rst @@ -31,7 +31,7 @@ You can then restrict a test run to only run tests marked with ``webtest``:: $ pytest -v -m webtest =========================== test session starts ============================ - platform linux -- Python 3.x.y, pytest-3.x.y, py-1.x.y, pluggy-0.x.y -- $PYTHON_PREFIX/bin/python + platform linux -- Python 3.x.y, pytest-3.x.y, py-1.x.y, pluggy-0.x.y -- $PYTHON_PREFIX/bin/python3.6 cachedir: .pytest_cache rootdir: $REGENDOC_TMPDIR, inifile: collecting ... collected 4 items / 3 deselected @@ -44,7 +44,7 @@ Or the inverse, running all tests except the webtest ones:: $ pytest -v -m "not webtest" =========================== test session starts ============================ - platform linux -- Python 3.x.y, pytest-3.x.y, py-1.x.y, pluggy-0.x.y -- $PYTHON_PREFIX/bin/python + platform linux -- Python 3.x.y, pytest-3.x.y, py-1.x.y, pluggy-0.x.y -- $PYTHON_PREFIX/bin/python3.6 cachedir: .pytest_cache rootdir: $REGENDOC_TMPDIR, inifile: collecting ... collected 4 items / 1 deselected @@ -64,7 +64,7 @@ tests based on their module, class, method, or function name:: $ pytest -v test_server.py::TestClass::test_method =========================== test session starts ============================ - platform linux -- Python 3.x.y, pytest-3.x.y, py-1.x.y, pluggy-0.x.y -- $PYTHON_PREFIX/bin/python + platform linux -- Python 3.x.y, pytest-3.x.y, py-1.x.y, pluggy-0.x.y -- $PYTHON_PREFIX/bin/python3.6 cachedir: .pytest_cache rootdir: $REGENDOC_TMPDIR, inifile: collecting ... collected 1 item @@ -77,7 +77,7 @@ You can also select on the class:: $ pytest -v test_server.py::TestClass =========================== test session starts ============================ - platform linux -- Python 3.x.y, pytest-3.x.y, py-1.x.y, pluggy-0.x.y -- $PYTHON_PREFIX/bin/python + platform linux -- Python 3.x.y, pytest-3.x.y, py-1.x.y, pluggy-0.x.y -- $PYTHON_PREFIX/bin/python3.6 cachedir: .pytest_cache rootdir: $REGENDOC_TMPDIR, inifile: collecting ... collected 1 item @@ -90,7 +90,7 @@ Or select multiple nodes:: $ pytest -v test_server.py::TestClass test_server.py::test_send_http =========================== test session starts ============================ - platform linux -- Python 3.x.y, pytest-3.x.y, py-1.x.y, pluggy-0.x.y -- $PYTHON_PREFIX/bin/python + platform linux -- Python 3.x.y, pytest-3.x.y, py-1.x.y, pluggy-0.x.y -- $PYTHON_PREFIX/bin/python3.6 cachedir: .pytest_cache rootdir: $REGENDOC_TMPDIR, inifile: collecting ... collected 2 items @@ -128,7 +128,7 @@ select tests based on their names:: $ pytest -v -k http # running with the above defined example module =========================== test session starts ============================ - platform linux -- Python 3.x.y, pytest-3.x.y, py-1.x.y, pluggy-0.x.y -- $PYTHON_PREFIX/bin/python + platform linux -- Python 3.x.y, pytest-3.x.y, py-1.x.y, pluggy-0.x.y -- $PYTHON_PREFIX/bin/python3.6 cachedir: .pytest_cache rootdir: $REGENDOC_TMPDIR, inifile: collecting ... collected 4 items / 3 deselected @@ -141,7 +141,7 @@ And you can also run all tests except the ones that match the keyword:: $ pytest -k "not send_http" -v =========================== test session starts ============================ - platform linux -- Python 3.x.y, pytest-3.x.y, py-1.x.y, pluggy-0.x.y -- $PYTHON_PREFIX/bin/python + platform linux -- Python 3.x.y, pytest-3.x.y, py-1.x.y, pluggy-0.x.y -- $PYTHON_PREFIX/bin/python3.6 cachedir: .pytest_cache rootdir: $REGENDOC_TMPDIR, inifile: collecting ... collected 4 items / 1 deselected @@ -156,7 +156,7 @@ Or to select "http" and "quick" tests:: $ pytest -k "http or quick" -v =========================== test session starts ============================ - platform linux -- Python 3.x.y, pytest-3.x.y, py-1.x.y, pluggy-0.x.y -- $PYTHON_PREFIX/bin/python + platform linux -- Python 3.x.y, pytest-3.x.y, py-1.x.y, pluggy-0.x.y -- $PYTHON_PREFIX/bin/python3.6 cachedir: .pytest_cache rootdir: $REGENDOC_TMPDIR, inifile: collecting ... collected 4 items / 2 deselected diff --git a/doc/en/example/nonpython.rst b/doc/en/example/nonpython.rst index a266b5bfe..bda15065a 100644 --- a/doc/en/example/nonpython.rst +++ b/doc/en/example/nonpython.rst @@ -59,7 +59,7 @@ consulted when reporting in ``verbose`` mode:: nonpython $ pytest -v =========================== test session starts ============================ - platform linux -- Python 3.x.y, pytest-3.x.y, py-1.x.y, pluggy-0.x.y -- $PYTHON_PREFIX/bin/python + platform linux -- Python 3.x.y, pytest-3.x.y, py-1.x.y, pluggy-0.x.y -- $PYTHON_PREFIX/bin/python3.6 cachedir: .pytest_cache rootdir: $REGENDOC_TMPDIR/nonpython, inifile: collecting ... collected 2 items diff --git a/doc/en/example/simple.rst b/doc/en/example/simple.rst index 7ec27d547..b16070287 100644 --- a/doc/en/example/simple.rst +++ b/doc/en/example/simple.rst @@ -357,7 +357,7 @@ which will add info only when run with "--v":: $ pytest -v =========================== test session starts ============================ - platform linux -- Python 3.x.y, pytest-3.x.y, py-1.x.y, pluggy-0.x.y -- $PYTHON_PREFIX/bin/python + platform linux -- Python 3.x.y, pytest-3.x.y, py-1.x.y, pluggy-0.x.y -- $PYTHON_PREFIX/bin/python3.6 cachedir: .pytest_cache info1: did you know that ... did you? @@ -574,7 +574,7 @@ We can run this:: file $REGENDOC_TMPDIR/b/test_error.py, line 1 def test_root(db): # no db here, will error out E fixture 'db' not found - > available fixtures: cache, capfd, capfdbinary, caplog, capsys, capsysbinary, doctest_namespace, monkeypatch, pytestconfig, record_property, record_xml_attribute, record_xml_property, recwarn, tmpdir, tmpdir_factory + > available fixtures: cache, capfd, capfdbinary, caplog, capsys, capsysbinary, doctest_namespace, monkeypatch, pytestconfig, record_property, record_xml_attribute, record_xml_property, recwarn, tmp_path, tmp_path_factory, tmpdir, tmpdir_factory > use 'pytest --fixtures [testpath]' for help on them. $REGENDOC_TMPDIR/b/test_error.py:1 diff --git a/doc/en/fixture.rst b/doc/en/fixture.rst index fa111afa4..65664c0b2 100644 --- a/doc/en/fixture.rst +++ b/doc/en/fixture.rst @@ -732,7 +732,7 @@ Running this test will *skip* the invocation of ``data_set`` with value ``2``:: $ pytest test_fixture_marks.py -v =========================== test session starts ============================ - platform linux -- Python 3.x.y, pytest-3.x.y, py-1.x.y, pluggy-0.x.y -- $PYTHON_PREFIX/bin/python + platform linux -- Python 3.x.y, pytest-3.x.y, py-1.x.y, pluggy-0.x.y -- $PYTHON_PREFIX/bin/python3.6 cachedir: .pytest_cache rootdir: $REGENDOC_TMPDIR, inifile: collecting ... collected 3 items @@ -775,7 +775,7 @@ Here we declare an ``app`` fixture which receives the previously defined $ pytest -v test_appsetup.py =========================== test session starts ============================ - platform linux -- Python 3.x.y, pytest-3.x.y, py-1.x.y, pluggy-0.x.y -- $PYTHON_PREFIX/bin/python + platform linux -- Python 3.x.y, pytest-3.x.y, py-1.x.y, pluggy-0.x.y -- $PYTHON_PREFIX/bin/python3.6 cachedir: .pytest_cache rootdir: $REGENDOC_TMPDIR, inifile: collecting ... collected 2 items @@ -844,7 +844,7 @@ Let's run the tests in verbose mode and with looking at the print-output:: $ pytest -v -s test_module.py =========================== test session starts ============================ - platform linux -- Python 3.x.y, pytest-3.x.y, py-1.x.y, pluggy-0.x.y -- $PYTHON_PREFIX/bin/python + platform linux -- Python 3.x.y, pytest-3.x.y, py-1.x.y, pluggy-0.x.y -- $PYTHON_PREFIX/bin/python3.6 cachedir: .pytest_cache rootdir: $REGENDOC_TMPDIR, inifile: collecting ... collected 8 items diff --git a/doc/en/tmpdir.rst b/doc/en/tmpdir.rst index aff9a9308..d8cd8b705 100644 --- a/doc/en/tmpdir.rst +++ b/doc/en/tmpdir.rst @@ -38,9 +38,30 @@ Running this would result in a passed test except for the last ``assert 0`` line which we use to look at values:: $ pytest test_tmp_path.py - ... #fill fom regendoc + =========================== test session starts ============================ + platform linux -- Python 3.x.y, pytest-3.x.y, py-1.x.y, pluggy-0.x.y + rootdir: $REGENDOC_TMPDIR, inifile: + collected 1 item + test_tmp_path.py F [100%] + ================================= FAILURES ================================= + _____________________________ test_create_file _____________________________ + + tmp_path = PosixPath('PYTEST_TMPDIR/test_create_file0') + + def test_create_file(tmp_path): + d = tmp_path / "sub" + d.mkdir() + p = d / "hello.txt" + p.write_text(CONTENT) + assert p.read_text() == CONTENT + assert len(list(tmp_path.iterdir())) == 1 + > assert 0 + E assert 0 + + test_tmp_path.py:13: AssertionError + ========================= 1 failed in 0.12 seconds ========================= The ``tmp_path_factory`` fixture -------------------------------- diff --git a/doc/en/writing_plugins.rst b/doc/en/writing_plugins.rst index 70e48f817..2cb1caefb 100644 --- a/doc/en/writing_plugins.rst +++ b/doc/en/writing_plugins.rst @@ -420,9 +420,21 @@ additionally it is possible to copy examples for a example folder before running ============================= warnings summary ============================= $REGENDOC_TMPDIR/test_example.py:4: PytestExperimentalApiWarning: testdir.copy_example is an experimental api that may change over time testdir.copy_example("test_example.py") + $PYTHON_PREFIX/lib/python3.6/site-packages/_pytest/compat.py:321: RemovedInPytest4Warning: usage of Session.Class is deprecated, please use pytest.Class instead + return getattr(object, name, default) + $PYTHON_PREFIX/lib/python3.6/site-packages/_pytest/compat.py:321: RemovedInPytest4Warning: usage of Session.File is deprecated, please use pytest.File instead + return getattr(object, name, default) + $PYTHON_PREFIX/lib/python3.6/site-packages/_pytest/compat.py:321: RemovedInPytest4Warning: usage of Session.Function is deprecated, please use pytest.Function instead + return getattr(object, name, default) + $PYTHON_PREFIX/lib/python3.6/site-packages/_pytest/compat.py:321: RemovedInPytest4Warning: usage of Session.Instance is deprecated, please use pytest.Instance instead + return getattr(object, name, default) + $PYTHON_PREFIX/lib/python3.6/site-packages/_pytest/compat.py:321: RemovedInPytest4Warning: usage of Session.Item is deprecated, please use pytest.Item instead + return getattr(object, name, default) + $PYTHON_PREFIX/lib/python3.6/site-packages/_pytest/compat.py:321: RemovedInPytest4Warning: usage of Session.Module is deprecated, please use pytest.Module instead + return getattr(object, name, default) -- Docs: https://docs.pytest.org/en/latest/warnings.html - =================== 2 passed, 1 warnings in 0.12 seconds =================== + =================== 2 passed, 7 warnings in 0.12 seconds =================== For more information about the result object that ``runpytest()`` returns, and the methods that it provides please check out the :py:class:`RunResult From 456715a5c1b4e573cb87b9864c1ca83616944395 Mon Sep 17 00:00:00 2001 From: Ronny Pfannschmidt Date: Tue, 16 Oct 2018 21:48:39 +0200 Subject: [PATCH 103/105] fix url quotes in setup.cfg metadata --- setup.cfg | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.cfg b/setup.cfg index a898b66c6..8cd3858fd 100644 --- a/setup.cfg +++ b/setup.cfg @@ -3,7 +3,7 @@ name = pytest description = pytest: simple powerful testing with Python long_description = file: README.rst -url = "https://docs.pytest.org/en/latest/" +url = https://docs.pytest.org/en/latest/ project_urls = Source=https://github.com/pytest-dev/pytest Tracker=https://github.com/pytest-dev/pytest/issues From 253c5786af681604f4e0c2b315f17fa5bcc950c2 Mon Sep 17 00:00:00 2001 From: Ronny Pfannschmidt Date: Tue, 16 Oct 2018 22:11:40 +0200 Subject: [PATCH 104/105] Preparing release version 3.9.1 --- CHANGELOG.rst | 11 +++++++++++ changelog/4159.feature.rst | 3 --- doc/en/announce/index.rst | 1 + doc/en/announce/release-3.9.1.rst | 20 ++++++++++++++++++++ doc/en/example/markers.rst | 16 ++++++++-------- doc/en/example/nonpython.rst | 2 +- doc/en/example/simple.rst | 2 +- doc/en/fixture.rst | 6 +++--- 8 files changed, 45 insertions(+), 16 deletions(-) delete mode 100644 changelog/4159.feature.rst create mode 100644 doc/en/announce/release-3.9.1.rst diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 0f0193864..7a5c83a00 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -18,6 +18,17 @@ with advance notice in the **Deprecations** section of releases. .. towncrier release notes start +pytest 3.9.1 (2018-10-16) +========================= + +Features +-------- + +- `#4159 `_: For test-suites containing test classes, the information about the subclassed + module is now output only if a higher verbosity level is specified (at least + "-vv"). + + pytest 3.9.0 (2018-10-15) ========================= diff --git a/changelog/4159.feature.rst b/changelog/4159.feature.rst deleted file mode 100644 index 28ca7462e..000000000 --- a/changelog/4159.feature.rst +++ /dev/null @@ -1,3 +0,0 @@ -For test-suites containing test classes, the information about the subclassed -module is now output only if a higher verbosity level is specified (at least -"-vv"). diff --git a/doc/en/announce/index.rst b/doc/en/announce/index.rst index 4dad6fd4f..a692eee15 100644 --- a/doc/en/announce/index.rst +++ b/doc/en/announce/index.rst @@ -6,6 +6,7 @@ Release announcements :maxdepth: 2 + release-3.9.1 release-3.9.0 release-3.8.2 release-3.8.1 diff --git a/doc/en/announce/release-3.9.1.rst b/doc/en/announce/release-3.9.1.rst new file mode 100644 index 000000000..f050e4653 --- /dev/null +++ b/doc/en/announce/release-3.9.1.rst @@ -0,0 +1,20 @@ +pytest-3.9.1 +======================================= + +pytest 3.9.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: + +* Bruno Oliveira +* Ronny Pfannschmidt +* Thomas Hisch + + +Happy testing, +The pytest Development Team diff --git a/doc/en/example/markers.rst b/doc/en/example/markers.rst index cb6368a64..a2cb8a676 100644 --- a/doc/en/example/markers.rst +++ b/doc/en/example/markers.rst @@ -31,7 +31,7 @@ You can then restrict a test run to only run tests marked with ``webtest``:: $ pytest -v -m webtest =========================== test session starts ============================ - platform linux -- Python 3.x.y, pytest-3.x.y, py-1.x.y, pluggy-0.x.y -- $PYTHON_PREFIX/bin/python3.6 + platform linux -- Python 3.x.y, pytest-3.x.y, py-1.x.y, pluggy-0.x.y -- $PYTHON_PREFIX/bin/python cachedir: .pytest_cache rootdir: $REGENDOC_TMPDIR, inifile: collecting ... collected 4 items / 3 deselected @@ -44,7 +44,7 @@ Or the inverse, running all tests except the webtest ones:: $ pytest -v -m "not webtest" =========================== test session starts ============================ - platform linux -- Python 3.x.y, pytest-3.x.y, py-1.x.y, pluggy-0.x.y -- $PYTHON_PREFIX/bin/python3.6 + platform linux -- Python 3.x.y, pytest-3.x.y, py-1.x.y, pluggy-0.x.y -- $PYTHON_PREFIX/bin/python cachedir: .pytest_cache rootdir: $REGENDOC_TMPDIR, inifile: collecting ... collected 4 items / 1 deselected @@ -64,7 +64,7 @@ tests based on their module, class, method, or function name:: $ pytest -v test_server.py::TestClass::test_method =========================== test session starts ============================ - platform linux -- Python 3.x.y, pytest-3.x.y, py-1.x.y, pluggy-0.x.y -- $PYTHON_PREFIX/bin/python3.6 + platform linux -- Python 3.x.y, pytest-3.x.y, py-1.x.y, pluggy-0.x.y -- $PYTHON_PREFIX/bin/python cachedir: .pytest_cache rootdir: $REGENDOC_TMPDIR, inifile: collecting ... collected 1 item @@ -77,7 +77,7 @@ You can also select on the class:: $ pytest -v test_server.py::TestClass =========================== test session starts ============================ - platform linux -- Python 3.x.y, pytest-3.x.y, py-1.x.y, pluggy-0.x.y -- $PYTHON_PREFIX/bin/python3.6 + platform linux -- Python 3.x.y, pytest-3.x.y, py-1.x.y, pluggy-0.x.y -- $PYTHON_PREFIX/bin/python cachedir: .pytest_cache rootdir: $REGENDOC_TMPDIR, inifile: collecting ... collected 1 item @@ -90,7 +90,7 @@ Or select multiple nodes:: $ pytest -v test_server.py::TestClass test_server.py::test_send_http =========================== test session starts ============================ - platform linux -- Python 3.x.y, pytest-3.x.y, py-1.x.y, pluggy-0.x.y -- $PYTHON_PREFIX/bin/python3.6 + platform linux -- Python 3.x.y, pytest-3.x.y, py-1.x.y, pluggy-0.x.y -- $PYTHON_PREFIX/bin/python cachedir: .pytest_cache rootdir: $REGENDOC_TMPDIR, inifile: collecting ... collected 2 items @@ -128,7 +128,7 @@ select tests based on their names:: $ pytest -v -k http # running with the above defined example module =========================== test session starts ============================ - platform linux -- Python 3.x.y, pytest-3.x.y, py-1.x.y, pluggy-0.x.y -- $PYTHON_PREFIX/bin/python3.6 + platform linux -- Python 3.x.y, pytest-3.x.y, py-1.x.y, pluggy-0.x.y -- $PYTHON_PREFIX/bin/python cachedir: .pytest_cache rootdir: $REGENDOC_TMPDIR, inifile: collecting ... collected 4 items / 3 deselected @@ -141,7 +141,7 @@ And you can also run all tests except the ones that match the keyword:: $ pytest -k "not send_http" -v =========================== test session starts ============================ - platform linux -- Python 3.x.y, pytest-3.x.y, py-1.x.y, pluggy-0.x.y -- $PYTHON_PREFIX/bin/python3.6 + platform linux -- Python 3.x.y, pytest-3.x.y, py-1.x.y, pluggy-0.x.y -- $PYTHON_PREFIX/bin/python cachedir: .pytest_cache rootdir: $REGENDOC_TMPDIR, inifile: collecting ... collected 4 items / 1 deselected @@ -156,7 +156,7 @@ Or to select "http" and "quick" tests:: $ pytest -k "http or quick" -v =========================== test session starts ============================ - platform linux -- Python 3.x.y, pytest-3.x.y, py-1.x.y, pluggy-0.x.y -- $PYTHON_PREFIX/bin/python3.6 + platform linux -- Python 3.x.y, pytest-3.x.y, py-1.x.y, pluggy-0.x.y -- $PYTHON_PREFIX/bin/python cachedir: .pytest_cache rootdir: $REGENDOC_TMPDIR, inifile: collecting ... collected 4 items / 2 deselected diff --git a/doc/en/example/nonpython.rst b/doc/en/example/nonpython.rst index bda15065a..a266b5bfe 100644 --- a/doc/en/example/nonpython.rst +++ b/doc/en/example/nonpython.rst @@ -59,7 +59,7 @@ consulted when reporting in ``verbose`` mode:: nonpython $ pytest -v =========================== test session starts ============================ - platform linux -- Python 3.x.y, pytest-3.x.y, py-1.x.y, pluggy-0.x.y -- $PYTHON_PREFIX/bin/python3.6 + platform linux -- Python 3.x.y, pytest-3.x.y, py-1.x.y, pluggy-0.x.y -- $PYTHON_PREFIX/bin/python cachedir: .pytest_cache rootdir: $REGENDOC_TMPDIR/nonpython, inifile: collecting ... collected 2 items diff --git a/doc/en/example/simple.rst b/doc/en/example/simple.rst index b16070287..df83ec97e 100644 --- a/doc/en/example/simple.rst +++ b/doc/en/example/simple.rst @@ -357,7 +357,7 @@ which will add info only when run with "--v":: $ pytest -v =========================== test session starts ============================ - platform linux -- Python 3.x.y, pytest-3.x.y, py-1.x.y, pluggy-0.x.y -- $PYTHON_PREFIX/bin/python3.6 + platform linux -- Python 3.x.y, pytest-3.x.y, py-1.x.y, pluggy-0.x.y -- $PYTHON_PREFIX/bin/python cachedir: .pytest_cache info1: did you know that ... did you? diff --git a/doc/en/fixture.rst b/doc/en/fixture.rst index 65664c0b2..fa111afa4 100644 --- a/doc/en/fixture.rst +++ b/doc/en/fixture.rst @@ -732,7 +732,7 @@ Running this test will *skip* the invocation of ``data_set`` with value ``2``:: $ pytest test_fixture_marks.py -v =========================== test session starts ============================ - platform linux -- Python 3.x.y, pytest-3.x.y, py-1.x.y, pluggy-0.x.y -- $PYTHON_PREFIX/bin/python3.6 + platform linux -- Python 3.x.y, pytest-3.x.y, py-1.x.y, pluggy-0.x.y -- $PYTHON_PREFIX/bin/python cachedir: .pytest_cache rootdir: $REGENDOC_TMPDIR, inifile: collecting ... collected 3 items @@ -775,7 +775,7 @@ Here we declare an ``app`` fixture which receives the previously defined $ pytest -v test_appsetup.py =========================== test session starts ============================ - platform linux -- Python 3.x.y, pytest-3.x.y, py-1.x.y, pluggy-0.x.y -- $PYTHON_PREFIX/bin/python3.6 + platform linux -- Python 3.x.y, pytest-3.x.y, py-1.x.y, pluggy-0.x.y -- $PYTHON_PREFIX/bin/python cachedir: .pytest_cache rootdir: $REGENDOC_TMPDIR, inifile: collecting ... collected 2 items @@ -844,7 +844,7 @@ Let's run the tests in verbose mode and with looking at the print-output:: $ pytest -v -s test_module.py =========================== test session starts ============================ - platform linux -- Python 3.x.y, pytest-3.x.y, py-1.x.y, pluggy-0.x.y -- $PYTHON_PREFIX/bin/python3.6 + platform linux -- Python 3.x.y, pytest-3.x.y, py-1.x.y, pluggy-0.x.y -- $PYTHON_PREFIX/bin/python cachedir: .pytest_cache rootdir: $REGENDOC_TMPDIR, inifile: collecting ... collected 8 items From c5bbf8ac73a78fd15016296e808d13f2a2a983f1 Mon Sep 17 00:00:00 2001 From: Ronny Pfannschmidt Date: Tue, 16 Oct 2018 22:21:15 +0200 Subject: [PATCH 105/105] make note about the unpublished 3.9.0 release --- CHANGELOG.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 7a5c83a00..015f9dbd3 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -29,8 +29,8 @@ Features "-vv"). -pytest 3.9.0 (2018-10-15) -========================= +pytest 3.9.0 (2018-10-15 - not published due to a release automation bug) +========================================================================= Deprecations ------------