From b095e0de47a684bf7a7a3a7ec49c5457fabf6903 Mon Sep 17 00:00:00 2001 From: Tim Hoffmann <2836374+timhoffm@users.noreply.github.com> Date: Fri, 9 Aug 2019 21:35:03 +0200 Subject: [PATCH 01/11] Improve docs of pytest.importorskip --- doc/en/reference.rst | 2 ++ doc/en/skipping.rst | 9 ++++----- src/_pytest/outcomes.py | 19 +++++++++++++------ 3 files changed, 19 insertions(+), 11 deletions(-) diff --git a/doc/en/reference.rst b/doc/en/reference.rst index 7e47ffce072..991050c2326 100644 --- a/doc/en/reference.rst +++ b/doc/en/reference.rst @@ -27,6 +27,8 @@ pytest.skip .. autofunction:: _pytest.outcomes.skip(msg, [allow_module_level=False]) +.. _`pytest.importorskip ref`: + pytest.importorskip ~~~~~~~~~~~~~~~~~~~ diff --git a/doc/en/skipping.rst b/doc/en/skipping.rst index eb00a6dfb12..57f565472e5 100644 --- a/doc/en/skipping.rst +++ b/doc/en/skipping.rst @@ -179,16 +179,15 @@ information. Skipping on a missing import dependency ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -You can use the following helper at module level -or within a test or test setup function: +You can skip tests on a missing import by using :ref:`pytest.importorskip ref` +at module level or within a test or test setup function. .. code-block:: python docutils = pytest.importorskip("docutils") -If ``docutils`` cannot be imported here, this will lead to a -skip outcome of the test. You can also skip based on the -version number of a library: +If ``docutils`` cannot be imported here, this will lead to a skip outcome of +the test. You can also skip based on the version number of a library: .. code-block:: python diff --git a/src/_pytest/outcomes.py b/src/_pytest/outcomes.py index 55c30803f59..a3b6d405430 100644 --- a/src/_pytest/outcomes.py +++ b/src/_pytest/outcomes.py @@ -143,14 +143,21 @@ def xfail(reason=""): def importorskip(modname, minversion=None, reason=None): - """Imports and returns the requested module ``modname``, or skip the current test - if the module cannot be imported. + """Imports and returns the requested module ``modname``, or skip the + current test if the module cannot be imported. :param str modname: the name of the module to import - :param str minversion: if given, the imported module ``__version__`` attribute must be - at least this minimal version, otherwise the test is still skipped. - :param str reason: if given, this reason is shown as the message when the module - cannot be imported. + :param str minversion: if given, the imported module ``__version__`` + attribute must be at least this minimal version, otherwise the test is + still skipped. + :param str reason: if given, this reason is shown as the message when the + module cannot be imported. + :returns: The imported module. This should be assigned to its canonical + name. + + Example:: + + docutils = pytest.importorskip("docutils") """ import warnings From e0ce8b79d5bb6d1114201ce5d85fd6f02948e325 Mon Sep 17 00:00:00 2001 From: martbln Date: Sat, 10 Aug 2019 00:55:39 +0300 Subject: [PATCH 02/11] pytester: add docstrings for Testdir.copy_example --- changelog/5669.doc.rst | 1 + src/_pytest/pytester.py | 6 ++++++ 2 files changed, 7 insertions(+) create mode 100644 changelog/5669.doc.rst diff --git a/changelog/5669.doc.rst b/changelog/5669.doc.rst new file mode 100644 index 00000000000..0ec9626ae50 --- /dev/null +++ b/changelog/5669.doc.rst @@ -0,0 +1 @@ +Add docstring for ``Testdir.copy_example``. diff --git a/src/_pytest/pytester.py b/src/_pytest/pytester.py index 2068761faf9..eef87615253 100644 --- a/src/_pytest/pytester.py +++ b/src/_pytest/pytester.py @@ -630,6 +630,12 @@ def mkpydir(self, name): return p def copy_example(self, name=None): + """Copy file from project's directory into the testdir. + + :param str name: The name of the file for copy. + :return: self.tmpdir + + """ import warnings from _pytest.warning_types import PYTESTER_COPY_EXAMPLE From ee936b27a8bda9e46af327ce093c209463b488ab Mon Sep 17 00:00:00 2001 From: Ilya Stepin Date: Sat, 10 Aug 2019 09:12:04 +0300 Subject: [PATCH 03/11] pytester: fix docstrings Co-Authored-By: Bruno Oliveira --- 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 eef87615253..29c200ce6dc 100644 --- a/src/_pytest/pytester.py +++ b/src/_pytest/pytester.py @@ -633,7 +633,7 @@ def copy_example(self, name=None): """Copy file from project's directory into the testdir. :param str name: The name of the file for copy. - :return: self.tmpdir + :return: path to the copied directory (inside ``self.tmpdir``). """ import warnings From 0767f080a4dfe4fdcfbab4e66bfdccff4ecad52e Mon Sep 17 00:00:00 2001 From: Jon Dufresne Date: Sat, 10 Aug 2019 12:38:13 -0700 Subject: [PATCH 04/11] =?UTF-8?q?Update=20URL:=20python/black=20=E2=86=92?= =?UTF-8?q?=20psf/black?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .pre-commit-config.yaml | 2 +- CONTRIBUTING.rst | 2 +- README.rst | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index c9940674d2a..13cdb855170 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,6 +1,6 @@ exclude: doc/en/example/py2py3/test_py2.py repos: -- repo: https://github.com/python/black +- repo: https://github.com/psf/black rev: 19.3b0 hooks: - id: black diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst index 5ef418e0b5c..8e59191abd4 100644 --- a/CONTRIBUTING.rst +++ b/CONTRIBUTING.rst @@ -167,7 +167,7 @@ Short version #. Enable and install `pre-commit `_ to ensure style-guides and code checks are followed. #. Target ``master`` for bugfixes and doc changes. #. Target ``features`` for new features or functionality changes. -#. Follow **PEP-8** for naming and `black `_ for formatting. +#. Follow **PEP-8** for naming and `black `_ for formatting. #. Tests are run using ``tox``:: tox -e linting,py37 diff --git a/README.rst b/README.rst index 301e4953884..4bf9cce2f48 100644 --- a/README.rst +++ b/README.rst @@ -26,7 +26,7 @@ :target: https://dev.azure.com/pytest-dev/pytest .. image:: https://img.shields.io/badge/code%20style-black-000000.svg - :target: https://github.com/python/black + :target: https://github.com/psf/black .. image:: https://www.codetriage.com/pytest-dev/pytest/badges/users.svg :target: https://www.codetriage.com/pytest-dev/pytest From 0db9dade653d9fbaf830cefaf305c25193837b15 Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Sat, 10 Aug 2019 23:30:49 +0200 Subject: [PATCH 05/11] test_non_ascii_paste_text: mock call to urlopen Likely to fix flaky coverage due to requests failing sometimes. Ref: https://codecov.io/gh/pytest-dev/pytest/compare/f7e81dab9aa8d57498511e1b5655b53cbfbed0d0...83a1f4bd668fe337d42f909cc2ef2d8d986e9748/changes --- testing/test_pastebin.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/testing/test_pastebin.py b/testing/test_pastebin.py index fd443ed40da..9afa1e23f31 100644 --- a/testing/test_pastebin.py +++ b/testing/test_pastebin.py @@ -55,7 +55,7 @@ def test_skip(): ] ) - def test_non_ascii_paste_text(self, testdir): + def test_non_ascii_paste_text(self, testdir, pastebinlist): """Make sure that text which contains non-ascii characters is pasted correctly. See #1219. """ @@ -74,6 +74,7 @@ def test(): "*Sending information to Paste Service*", ] ) + assert len(pastebinlist) == 1 class TestPaste: From 3eb4973065c17d0e6904b5e622875a590af0bd0b Mon Sep 17 00:00:00 2001 From: boris Date: Mon, 12 Aug 2019 00:09:53 -0600 Subject: [PATCH 06/11] remove %s formatting from docs --- doc/en/capture.rst | 2 +- doc/en/example/assertion/failure_demo.py | 2 +- doc/en/example/markers.rst | 4 ++-- doc/en/example/multipython.py | 2 +- doc/en/example/nonpython/conftest.py | 4 ++-- doc/en/example/reportingdemo.rst | 2 +- doc/en/example/simple.rst | 4 ++-- doc/en/fixture.rst | 14 +++++++------- doc/en/getting-started.rst | 2 +- 9 files changed, 18 insertions(+), 18 deletions(-) diff --git a/doc/en/capture.rst b/doc/en/capture.rst index 356da26785e..55714c25b4b 100644 --- a/doc/en/capture.rst +++ b/doc/en/capture.rst @@ -57,7 +57,7 @@ is that you can use print statements for debugging: def setup_function(function): - print("setting up %s" % function) + print("setting up", function) def test_func1(): diff --git a/doc/en/example/assertion/failure_demo.py b/doc/en/example/assertion/failure_demo.py index 129362cd7ff..26454e48d76 100644 --- a/doc/en/example/assertion/failure_demo.py +++ b/doc/en/example/assertion/failure_demo.py @@ -177,7 +177,7 @@ def test_tupleerror(self): def test_reinterpret_fails_with_print_for_the_fun_of_it(self): items = [1, 2, 3] - print("items is %r" % items) + print("items is {!r}".format(items)) a, b = items.pop() def test_some_error(self): diff --git a/doc/en/example/markers.rst b/doc/en/example/markers.rst index a1fe658740b..909f23a2e13 100644 --- a/doc/en/example/markers.rst +++ b/doc/en/example/markers.rst @@ -384,7 +384,7 @@ specifies via named environments: envnames = [mark.args[0] for mark in item.iter_markers(name="env")] if envnames: if item.config.getoption("-E") not in envnames: - pytest.skip("test requires env in %r" % envnames) + pytest.skip("test requires env in {!r}".format(envnames)) A test file using this local plugin: @@ -578,7 +578,7 @@ for your particular platform, you could use the following plugin: supported_platforms = ALL.intersection(mark.name for mark in item.iter_markers()) plat = sys.platform if supported_platforms and plat not in supported_platforms: - pytest.skip("cannot run on platform %s" % (plat)) + pytest.skip("cannot run on platform {}".format(plat)) then tests will be skipped if they were specified for a different platform. Let's do a little test file to show how this looks like: diff --git a/doc/en/example/multipython.py b/doc/en/example/multipython.py index 3dc1d9b29d1..9db6879edae 100644 --- a/doc/en/example/multipython.py +++ b/doc/en/example/multipython.py @@ -69,4 +69,4 @@ def load_and_is_true(self, expression): @pytest.mark.parametrize("obj", [42, {}, {1: 3}]) def test_basic_objects(python1, python2, obj): python1.dumps(obj) - python2.load_and_is_true("obj == %s" % obj) + python2.load_and_is_true("obj == {}".format(obj)) diff --git a/doc/en/example/nonpython/conftest.py b/doc/en/example/nonpython/conftest.py index c94620e97ba..93d8285bfa7 100644 --- a/doc/en/example/nonpython/conftest.py +++ b/doc/en/example/nonpython/conftest.py @@ -33,13 +33,13 @@ def repr_failure(self, excinfo): return "\n".join( [ "usecase execution failed", - " spec failed: %r: %r" % excinfo.value.args[1:3], + " spec failed: {1!r}: {2!r}".format(*excinfo.value.args), " no further details known at this point.", ] ) def reportinfo(self): - return self.fspath, 0, "usecase: %s" % self.name + return self.fspath, 0, "usecase: {}".format(self.name) class YamlException(Exception): diff --git a/doc/en/example/reportingdemo.rst b/doc/en/example/reportingdemo.rst index ae8928c51b6..05d06ecb6be 100644 --- a/doc/en/example/reportingdemo.rst +++ b/doc/en/example/reportingdemo.rst @@ -434,7 +434,7 @@ Here is a nice run of several failures and how ``pytest`` presents things: def test_reinterpret_fails_with_print_for_the_fun_of_it(self): items = [1, 2, 3] - print("items is %r" % items) + print("items is {!r}".format(items)) > a, b = items.pop() E TypeError: 'int' object is not iterable diff --git a/doc/en/example/simple.rst b/doc/en/example/simple.rst index dbfba0fa09d..b4baa2b9b83 100644 --- a/doc/en/example/simple.rst +++ b/doc/en/example/simple.rst @@ -478,7 +478,7 @@ an ``incremental`` marker which is to be used on classes: if "incremental" in item.keywords: previousfailed = getattr(item.parent, "_previousfailed", None) if previousfailed is not None: - pytest.xfail("previous test failed (%s)" % previousfailed.name) + pytest.xfail("previous test failed ({})".format(previousfailed.name)) These two hook implementations work together to abort incremental-marked tests in a class. Here is a test module example: @@ -684,7 +684,7 @@ case we just write some information out to a ``failures`` file: with open("failures", mode) as f: # let's also access a fixture for the fun of it if "tmpdir" in item.fixturenames: - extra = " (%s)" % item.funcargs["tmpdir"] + extra = " ({})".format(item.funcargs["tmpdir"]) else: extra = "" diff --git a/doc/en/fixture.rst b/doc/en/fixture.rst index ea5e2dc86e8..b494ec0fe1d 100644 --- a/doc/en/fixture.rst +++ b/doc/en/fixture.rst @@ -629,7 +629,7 @@ through the special :py:class:`request ` object: def smtp_connection(request): smtp_connection = smtplib.SMTP(request.param, 587, timeout=5) yield smtp_connection - print("finalizing %s" % smtp_connection) + print("finalizing {}".format(smtp_connection)) smtp_connection.close() The main change is the declaration of ``params`` with @@ -902,25 +902,25 @@ to show the setup/teardown flow: @pytest.fixture(scope="module", params=["mod1", "mod2"]) def modarg(request): param = request.param - print(" SETUP modarg %s" % param) + print(" SETUP modarg", param) yield param - print(" TEARDOWN modarg %s" % param) + print(" TEARDOWN modarg", param) @pytest.fixture(scope="function", params=[1, 2]) def otherarg(request): param = request.param - print(" SETUP otherarg %s" % param) + print(" SETUP otherarg", param) yield param - print(" TEARDOWN otherarg %s" % param) + print(" TEARDOWN otherarg", param) def test_0(otherarg): - print(" RUN test0 with otherarg %s" % otherarg) + print(" RUN test0 with otherarg", otherarg) def test_1(modarg): - print(" RUN test1 with modarg %s" % modarg) + print(" RUN test1 with modarg", modarg) def test_2(otherarg, modarg): diff --git a/doc/en/getting-started.rst b/doc/en/getting-started.rst index 149bd33f163..f1c28769f0b 100644 --- a/doc/en/getting-started.rst +++ b/doc/en/getting-started.rst @@ -28,7 +28,7 @@ Install ``pytest`` .. code-block:: bash $ pytest --version - This is pytest version 5.x.y, imported from $PYTHON_PREFIX/lib/python3.6/site-packages/pytest.py + This is pytest version 5.x.y, imported from $PYTHON_PREFIX/lib/python3.x/site-packages/pytest.py .. _`simpletest`: From 7183335e6269980161ae9cc67679bf07d65bf1c5 Mon Sep 17 00:00:00 2001 From: "dmitry.dygalo" Date: Mon, 12 Aug 2019 07:47:40 +0200 Subject: [PATCH 07/11] Capture warnings during ``pytest_configure`` Fix #5115 --- changelog/5115.bugfix.rst | 1 + src/_pytest/config/__init__.py | 4 +++- testing/test_warnings.py | 18 ++++++++++++++++++ 3 files changed, 22 insertions(+), 1 deletion(-) create mode 100644 changelog/5115.bugfix.rst diff --git a/changelog/5115.bugfix.rst b/changelog/5115.bugfix.rst new file mode 100644 index 00000000000..af75499a395 --- /dev/null +++ b/changelog/5115.bugfix.rst @@ -0,0 +1 @@ +Warnings issued during ``pytest_configure`` are explicitly not treated as errors, even if configured as such, because it otherwise completely breaks pytest. diff --git a/src/_pytest/config/__init__.py b/src/_pytest/config/__init__.py index c1bd2e7ebd9..1ddf788298e 100644 --- a/src/_pytest/config/__init__.py +++ b/src/_pytest/config/__init__.py @@ -642,7 +642,9 @@ def add_cleanup(self, func): def _do_configure(self): assert not self._configured self._configured = True - self.hook.pytest_configure.call_historic(kwargs=dict(config=self)) + with warnings.catch_warnings(): + warnings.simplefilter("default") + self.hook.pytest_configure.call_historic(kwargs=dict(config=self)) def _ensure_unconfigure(self): if self._configured: diff --git a/testing/test_warnings.py b/testing/test_warnings.py index 2ce83ae8839..4580ab53f2d 100644 --- a/testing/test_warnings.py +++ b/testing/test_warnings.py @@ -645,3 +645,21 @@ def test_group_warnings_by_message(testdir): warning_code = 'warnings.warn(UserWarning("foo"))' assert warning_code in result.stdout.str() assert result.stdout.str().count(warning_code) == 1 + + +def test_pytest_configure_warning(testdir, recwarn): + """Issue 5115.""" + testdir.makeconftest( + """ + def pytest_configure(): + import warnings + + warnings.warn("from pytest_configure") + """ + ) + + result = testdir.runpytest() + assert result.ret == 5 + assert "INTERNALERROR" not in result.stderr.str() + warning = recwarn.pop() + assert str(warning.message) == "from pytest_configure" From 2f1b192fe67eb4ce1c60ad61dc377ec8523293b6 Mon Sep 17 00:00:00 2001 From: Thomas Grainger Date: Mon, 12 Aug 2019 21:58:10 +0100 Subject: [PATCH 08/11] Issue a warning for async gen functions Co-Authored-By: Bruno Oliveira --- changelog/5734.bugfix.rst | 1 + src/_pytest/compat.py | 14 ++++++++------ src/_pytest/python.py | 10 ++++++---- testing/acceptance_test.py | 32 ++++++++++++++++++++++++++++++-- testing/test_compat.py | 26 +++++++++++++++++++++++--- 5 files changed, 68 insertions(+), 15 deletions(-) create mode 100644 changelog/5734.bugfix.rst diff --git a/changelog/5734.bugfix.rst b/changelog/5734.bugfix.rst new file mode 100644 index 00000000000..dc20e6b523b --- /dev/null +++ b/changelog/5734.bugfix.rst @@ -0,0 +1 @@ +Skip async generator test functions, and update the warning message to refer to ``async def`` functions. diff --git a/src/_pytest/compat.py b/src/_pytest/compat.py index 52ffc36bc98..20be12d3e19 100644 --- a/src/_pytest/compat.py +++ b/src/_pytest/compat.py @@ -40,14 +40,16 @@ def is_generator(func): def iscoroutinefunction(func): - """Return True if func is a decorated coroutine function. + """ + Return True if func is a coroutine function (a function defined with async + def syntax, and doesn't contain yield), or a function decorated with + @asyncio.coroutine. - Note: copied and modified from Python 3.5's builtin couroutines.py to avoid import asyncio directly, - which in turns also initializes the "logging" module as side-effect (see issue #8). + Note: copied and modified from Python 3.5's builtin couroutines.py to avoid + importing asyncio directly, which in turns also initializes the "logging" + module as a side-effect (see issue #8). """ - return getattr(func, "_is_coroutine", False) or ( - hasattr(inspect, "iscoroutinefunction") and inspect.iscoroutinefunction(func) - ) + return inspect.iscoroutinefunction(func) or getattr(func, "_is_coroutine", False) def getlocation(function, curdir): diff --git a/src/_pytest/python.py b/src/_pytest/python.py index 66d8530602b..70b486fb14d 100644 --- a/src/_pytest/python.py +++ b/src/_pytest/python.py @@ -23,6 +23,7 @@ from _pytest.compat import getimfunc from _pytest.compat import getlocation from _pytest.compat import is_generator +from _pytest.compat import iscoroutinefunction from _pytest.compat import NOTSET from _pytest.compat import REGEX_TYPE from _pytest.compat import safe_getattr @@ -151,15 +152,16 @@ def pytest_configure(config): @hookimpl(trylast=True) def pytest_pyfunc_call(pyfuncitem): testfunction = pyfuncitem.obj - iscoroutinefunction = getattr(inspect, "iscoroutinefunction", None) - if iscoroutinefunction is not None and iscoroutinefunction(testfunction): - msg = "Coroutine functions are not natively supported and have been skipped.\n" + if iscoroutinefunction(testfunction) or ( + sys.version_info >= (3, 6) and inspect.isasyncgenfunction(testfunction) + ): + msg = "async def functions are not natively supported and have been skipped.\n" msg += "You need to install a suitable plugin for your async framework, for example:\n" msg += " - pytest-asyncio\n" msg += " - pytest-trio\n" msg += " - pytest-tornasync" warnings.warn(PytestUnhandledCoroutineWarning(msg.format(pyfuncitem.nodeid))) - skip(msg="coroutine function and no async plugin installed (see warnings)") + skip(msg="async def function and no async plugin installed (see warnings)") funcargs = pyfuncitem.funcargs testargs = {arg: funcargs[arg] for arg in pyfuncitem._fixtureinfo.argnames} testfunction(**testargs) diff --git a/testing/acceptance_test.py b/testing/acceptance_test.py index d2a348f40a9..55e3b966df0 100644 --- a/testing/acceptance_test.py +++ b/testing/acceptance_test.py @@ -1199,11 +1199,39 @@ async def test_2(): [ "test_async.py::test_1", "test_async.py::test_2", - "*Coroutine functions are not natively supported*", + "*async def functions are not natively supported*", "*2 skipped, 2 warnings in*", ] ) # ensure our warning message appears only once assert ( - result.stdout.str().count("Coroutine functions are not natively supported") == 1 + result.stdout.str().count("async def functions are not natively supported") == 1 + ) + + +@pytest.mark.filterwarnings("default") +@pytest.mark.skipif( + sys.version_info < (3, 6), reason="async gen syntax available in Python 3.6+" +) +def test_warn_on_async_gen_function(testdir): + testdir.makepyfile( + test_async=""" + async def test_1(): + yield + async def test_2(): + yield + """ + ) + result = testdir.runpytest() + result.stdout.fnmatch_lines( + [ + "test_async.py::test_1", + "test_async.py::test_2", + "*async def functions are not natively supported*", + "*2 skipped, 2 warnings in*", + ] + ) + # ensure our warning message appears only once + assert ( + result.stdout.str().count("async def functions are not natively supported") == 1 ) diff --git a/testing/test_compat.py b/testing/test_compat.py index 9e7d05c5df5..0244c07acfa 100644 --- a/testing/test_compat.py +++ b/testing/test_compat.py @@ -91,9 +91,6 @@ def test_is_generator_asyncio(): result.stdout.fnmatch_lines(["*1 passed*"]) -@pytest.mark.skipif( - sys.version_info < (3, 5), reason="async syntax available in Python 3.5+" -) def test_is_generator_async_syntax(testdir): testdir.makepyfile( """ @@ -113,6 +110,29 @@ async def bar(): result.stdout.fnmatch_lines(["*1 passed*"]) +@pytest.mark.skipif( + sys.version_info < (3, 6), reason="async gen syntax available in Python 3.6+" +) +def test_is_generator_async_gen_syntax(testdir): + testdir.makepyfile( + """ + from _pytest.compat import is_generator + def test_is_generator_py36(): + async def foo(): + yield + await foo() + + async def bar(): + yield + + assert not is_generator(foo) + assert not is_generator(bar) + """ + ) + result = testdir.runpytest() + result.stdout.fnmatch_lines(["*1 passed*"]) + + class ErrorsHelper: @property def raise_exception(self): From 137255816ea18b0ce8ea8f18c19fb8232d3f77a2 Mon Sep 17 00:00:00 2001 From: Thomas Grainger Date: Tue, 6 Aug 2019 15:02:46 +0100 Subject: [PATCH 09/11] Fix collection of staticmethods defined with functools.partial Related to #5701 --- changelog/5701.bugfix.rst | 1 + src/_pytest/compat.py | 12 ++++++---- src/_pytest/fixtures.py | 4 ++-- testing/python/collect.py | 46 -------------------------------------- testing/python/fixtures.py | 46 +++++++++++++++++++++++++++++++++++--- testing/test_compat.py | 11 +++++++++ 6 files changed, 65 insertions(+), 55 deletions(-) create mode 100644 changelog/5701.bugfix.rst diff --git a/changelog/5701.bugfix.rst b/changelog/5701.bugfix.rst new file mode 100644 index 00000000000..b654e74479a --- /dev/null +++ b/changelog/5701.bugfix.rst @@ -0,0 +1 @@ +Fix collection of ``staticmethod`` objects defined with ``functools.partial``. diff --git a/src/_pytest/compat.py b/src/_pytest/compat.py index 52ffc36bc98..97c06e3ffc6 100644 --- a/src/_pytest/compat.py +++ b/src/_pytest/compat.py @@ -78,7 +78,7 @@ def num_mock_patch_args(function): ) -def getfuncargnames(function, is_method=False, cls=None): +def getfuncargnames(function, *, name: str = "", is_method=False, cls=None): """Returns the names of a function's mandatory arguments. This should return the names of all function arguments that: @@ -91,11 +91,12 @@ def getfuncargnames(function, is_method=False, cls=None): be treated as a bound method even though it's not unless, only in the case of cls, the function is a static method. + The name parameter should be the original name in which the function was collected. + @RonnyPfannschmidt: This function should be refactored when we revisit fixtures. The fixture mechanism should ask the node for the fixture names, and not try to obtain directly from the function object well after collection has occurred. - """ # The parameters attribute of a Signature object contains an # ordered mapping of parameter names to Parameter instances. This @@ -118,11 +119,14 @@ def getfuncargnames(function, is_method=False, cls=None): ) and p.default is Parameter.empty ) + if not name: + name = function.__name__ + # If this function should be treated as a bound method even though # it's passed as an unbound method or function, remove the first # parameter name. if is_method or ( - cls and not isinstance(cls.__dict__.get(function.__name__, None), staticmethod) + cls and not isinstance(cls.__dict__.get(name, None), staticmethod) ): arg_names = arg_names[1:] # Remove any names that will be replaced with mocks. @@ -245,7 +249,7 @@ def get_real_method(obj, holder): try: is_method = hasattr(obj, "__func__") obj = get_real_func(obj) - except Exception: + except Exception: # pragma: no cover return obj if is_method and hasattr(obj, "__get__") and callable(obj.__get__): obj = obj.__get__(holder) diff --git a/src/_pytest/fixtures.py b/src/_pytest/fixtures.py index 965a2e6e9e2..6a3e82907fc 100644 --- a/src/_pytest/fixtures.py +++ b/src/_pytest/fixtures.py @@ -828,7 +828,7 @@ def __init__( where=baseid, ) self.params = params - self.argnames = getfuncargnames(func, is_method=unittest) + self.argnames = getfuncargnames(func, name=argname, is_method=unittest) self.unittest = unittest self.ids = ids self._finalizers = [] @@ -1143,7 +1143,7 @@ def _get_direct_parametrize_args(self, node): def getfixtureinfo(self, node, func, cls, funcargs=True): if funcargs and not getattr(node, "nofuncargs", False): - argnames = getfuncargnames(func, cls=cls) + argnames = getfuncargnames(func, name=node.name, cls=cls) else: argnames = () diff --git a/testing/python/collect.py b/testing/python/collect.py index 61fc5857998..e6dd3e87088 100644 --- a/testing/python/collect.py +++ b/testing/python/collect.py @@ -1143,52 +1143,6 @@ class Test(object): assert result.ret == ExitCode.NO_TESTS_COLLECTED -def test_collect_functools_partial(testdir): - """ - Test that collection of functools.partial object works, and arguments - to the wrapped functions are dealt with correctly (see #811). - """ - testdir.makepyfile( - """ - import functools - import pytest - - @pytest.fixture - def fix1(): - return 'fix1' - - @pytest.fixture - def fix2(): - return 'fix2' - - def check1(i, fix1): - assert i == 2 - assert fix1 == 'fix1' - - def check2(fix1, i): - assert i == 2 - assert fix1 == 'fix1' - - def check3(fix1, i, fix2): - assert i == 2 - assert fix1 == 'fix1' - assert fix2 == 'fix2' - - test_ok_1 = functools.partial(check1, i=2) - test_ok_2 = functools.partial(check1, i=2, fix1='fix1') - test_ok_3 = functools.partial(check1, 2) - test_ok_4 = functools.partial(check2, i=2) - test_ok_5 = functools.partial(check3, i=2) - test_ok_6 = functools.partial(check3, i=2, fix1='fix1') - - test_fail_1 = functools.partial(check2, 2) - test_fail_2 = functools.partial(check3, 2) - """ - ) - result = testdir.inline_run() - result.assertoutcome(passed=6, failed=2) - - @pytest.mark.filterwarnings("default") def test_dont_collect_non_function_callable(testdir): """Test for issue https://github.com/pytest-dev/pytest/issues/331 diff --git a/testing/python/fixtures.py b/testing/python/fixtures.py index c0c230ccf2b..a85a0d73175 100644 --- a/testing/python/fixtures.py +++ b/testing/python/fixtures.py @@ -10,7 +10,9 @@ from _pytest.warnings import SHOW_PYTEST_WARNINGS_ARG -def test_getfuncargnames(): +def test_getfuncargnames_functions(): + """Test getfuncargnames for normal functions""" + def f(): pass @@ -31,18 +33,56 @@ def j(arg1, arg2, arg3="hello"): assert fixtures.getfuncargnames(j) == ("arg1", "arg2") + +def test_getfuncargnames_methods(): + """Test getfuncargnames for normal methods""" + class A: def f(self, arg1, arg2="hello"): pass + assert fixtures.getfuncargnames(A().f) == ("arg1",) + + +def test_getfuncargnames_staticmethod(): + """Test getfuncargnames for staticmethods""" + + class A: @staticmethod - def static(arg1, arg2): + def static(arg1, arg2, x=1): pass - assert fixtures.getfuncargnames(A().f) == ("arg1",) assert fixtures.getfuncargnames(A.static, cls=A) == ("arg1", "arg2") +def test_getfuncargnames_partial(): + """Check getfuncargnames for methods defined with functools.partial (#5701)""" + import functools + + def check(arg1, arg2, i): + pass + + class T: + test_ok = functools.partial(check, i=2) + + values = fixtures.getfuncargnames(T().test_ok, name="test_ok") + assert values == ("arg1", "arg2") + + +def test_getfuncargnames_staticmethod_partial(): + """Check getfuncargnames for staticmethods defined with functools.partial (#5701)""" + import functools + + def check(arg1, arg2, i): + pass + + class T: + test_ok = staticmethod(functools.partial(check, i=2)) + + values = fixtures.getfuncargnames(T().test_ok, name="test_ok") + assert values == ("arg1", "arg2") + + @pytest.mark.pytester_example_path("fixtures/fill_fixtures") class TestFillFixtures: def test_fillfuncargs_exposed(self): diff --git a/testing/test_compat.py b/testing/test_compat.py index 9e7d05c5df5..fb2470d07c6 100644 --- a/testing/test_compat.py +++ b/testing/test_compat.py @@ -1,4 +1,5 @@ import sys +from functools import partial from functools import wraps import pytest @@ -72,6 +73,16 @@ def func(): assert get_real_func(wrapped_func2) is wrapped_func +def test_get_real_func_partial(): + """Test get_real_func handles partial instances correctly""" + + def foo(x): + return x + + assert get_real_func(foo) is foo + assert get_real_func(partial(foo)) is foo + + def test_is_generator_asyncio(testdir): testdir.makepyfile( """ From 6b9d729ed3d6a495d0ca721dee3f32bd1c25f340 Mon Sep 17 00:00:00 2001 From: Thomas Grainger Date: Thu, 15 Aug 2019 12:03:06 +0100 Subject: [PATCH 10/11] also warn on awaitable or async iterable test results --- src/_pytest/python.py | 15 ++++++++++----- testing/acceptance_test.py | 10 ++++++++-- 2 files changed, 18 insertions(+), 7 deletions(-) diff --git a/src/_pytest/python.py b/src/_pytest/python.py index 70b486fb14d..951da792bd4 100644 --- a/src/_pytest/python.py +++ b/src/_pytest/python.py @@ -151,10 +151,7 @@ def pytest_configure(config): @hookimpl(trylast=True) def pytest_pyfunc_call(pyfuncitem): - testfunction = pyfuncitem.obj - if iscoroutinefunction(testfunction) or ( - sys.version_info >= (3, 6) and inspect.isasyncgenfunction(testfunction) - ): + def async_warn(): msg = "async def functions are not natively supported and have been skipped.\n" msg += "You need to install a suitable plugin for your async framework, for example:\n" msg += " - pytest-asyncio\n" @@ -162,9 +159,17 @@ def pytest_pyfunc_call(pyfuncitem): msg += " - pytest-tornasync" warnings.warn(PytestUnhandledCoroutineWarning(msg.format(pyfuncitem.nodeid))) skip(msg="async def function and no async plugin installed (see warnings)") + + testfunction = pyfuncitem.obj + if iscoroutinefunction(testfunction) or ( + sys.version_info >= (3, 6) and inspect.isasyncgenfunction(testfunction) + ): + async_warn() funcargs = pyfuncitem.funcargs testargs = {arg: funcargs[arg] for arg in pyfuncitem._fixtureinfo.argnames} - testfunction(**testargs) + result = testfunction(**testargs) + if hasattr(result, "__await__") or hasattr(result, "__aiter__"): + async_warn() return True diff --git a/testing/acceptance_test.py b/testing/acceptance_test.py index 55e3b966df0..7abbe734e75 100644 --- a/testing/acceptance_test.py +++ b/testing/acceptance_test.py @@ -1192,6 +1192,8 @@ async def test_1(): pass async def test_2(): pass + def test_3(): + return test_2() """ ) result = testdir.runpytest() @@ -1199,8 +1201,9 @@ async def test_2(): [ "test_async.py::test_1", "test_async.py::test_2", + "test_async.py::test_3", "*async def functions are not natively supported*", - "*2 skipped, 2 warnings in*", + "*3 skipped, 3 warnings in*", ] ) # ensure our warning message appears only once @@ -1220,6 +1223,8 @@ async def test_1(): yield async def test_2(): yield + def test_3(): + return test_2() """ ) result = testdir.runpytest() @@ -1227,8 +1232,9 @@ async def test_2(): [ "test_async.py::test_1", "test_async.py::test_2", + "test_async.py::test_3", "*async def functions are not natively supported*", - "*2 skipped, 2 warnings in*", + "*3 skipped, 3 warnings in*", ] ) # ensure our warning message appears only once From 1049a38cee2d8642635733f2a9ea0e776c0929db Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Thu, 15 Aug 2019 10:05:42 -0300 Subject: [PATCH 11/11] Fix wording as suggested in review of #5741 --- doc/en/skipping.rst | 2 +- src/_pytest/outcomes.py | 2 +- src/_pytest/pytester.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/doc/en/skipping.rst b/doc/en/skipping.rst index 57f565472e5..2b654560e10 100644 --- a/doc/en/skipping.rst +++ b/doc/en/skipping.rst @@ -180,7 +180,7 @@ Skipping on a missing import dependency ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ You can skip tests on a missing import by using :ref:`pytest.importorskip ref` -at module level or within a test or test setup function. +at module level, within a test, or test setup function. .. code-block:: python diff --git a/src/_pytest/outcomes.py b/src/_pytest/outcomes.py index 13eb0a295e7..94713662571 100644 --- a/src/_pytest/outcomes.py +++ b/src/_pytest/outcomes.py @@ -161,7 +161,7 @@ def importorskip( current test if the module cannot be imported. :param str modname: the name of the module to import - :param str minversion: if given, the imported module ``__version__`` + :param str minversion: if given, the imported module's ``__version__`` attribute must be at least this minimal version, otherwise the test is still skipped. :param str reason: if given, this reason is shown as the message when the diff --git a/src/_pytest/pytester.py b/src/_pytest/pytester.py index d9ceb0e7867..94058f70cba 100644 --- a/src/_pytest/pytester.py +++ b/src/_pytest/pytester.py @@ -632,7 +632,7 @@ def mkpydir(self, name): def copy_example(self, name=None): """Copy file from project's directory into the testdir. - :param str name: The name of the file for copy. + :param str name: The name of the file to copy. :return: path to the copied directory (inside ``self.tmpdir``). """