From 00097df5cdc383315e020039233cd8e7562aa316 Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Wed, 22 Jan 2020 13:04:30 +0100 Subject: [PATCH 01/14] tests: add test_plugin_loading_order Ref: https://github.com/pytest-dev/pytest/pull/6443 --- testing/test_config.py | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/testing/test_config.py b/testing/test_config.py index 498cbf7eb..cc54e5b23 100644 --- a/testing/test_config.py +++ b/testing/test_config.py @@ -670,6 +670,32 @@ def test_disable_plugin_autoload(testdir, monkeypatch, parse_args, should_load): assert has_loaded == should_load +def test_plugin_loading_order(testdir): + """Test order of plugin loading with `-p`.""" + p1 = testdir.makepyfile( + """ + def test_terminal_plugin(request): + import myplugin + assert myplugin.terminal_plugin == [True, True] + """, + **{ + "myplugin": """ + terminal_plugin = [] + + def pytest_configure(config): + terminal_plugin.append(bool(config.pluginmanager.get_plugin("terminalreporter"))) + + def pytest_sessionstart(session): + config = session.config + terminal_plugin.append(bool(config.pluginmanager.get_plugin("terminalreporter"))) + """ + } + ) + testdir.syspathinsert() + result = testdir.runpytest("-p", "myplugin", str(p1)) + assert result.ret == 0 + + def test_cmdline_processargs_simple(testdir): testdir.makeconftest( """ From fe343a79f817734cc95f28fee91452d7d7fd8c21 Mon Sep 17 00:00:00 2001 From: Hugo Date: Fri, 27 Dec 2019 19:39:57 +0200 Subject: [PATCH 02/14] Remove deprecated license_file from setup.cfg Starting with wheel 0.32.0 (2018-09-29), the `license_file` option is deprecated. * https://wheel.readthedocs.io/en/stable/news.html The wheel will continue to include `LICENSE`, it is now included automatically: * https://wheel.readthedocs.io/en/stable/user_guide.html#including-license-files-in-the-generated-wheel-file And `LICENSE` is still included in sdists thanks to setuptools-scm: * https://github.com/pytest-dev/pytest/pull/6348#issuecomment-567836331 --- setup.cfg | 1 - 1 file changed, 1 deletion(-) diff --git a/setup.cfg b/setup.cfg index 54b64af96..bef9f7871 100644 --- a/setup.cfg +++ b/setup.cfg @@ -10,7 +10,6 @@ project_urls = 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 From 039d582b52795e1682ec98370439081483920a95 Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Sat, 25 Jan 2020 10:58:26 +0100 Subject: [PATCH 03/14] Fix `EncodedFile.writelines` This is implemented by the underlying stream already, which additionally checks if the stream is not closed, and calls `write` per line. Ref/via: https://github.com/pytest-dev/pytest/pull/6558#issuecomment-578210807 --- changelog/6566.bugfix.rst | 1 + src/_pytest/capture.py | 6 +++--- testing/test_capture.py | 12 ++++++++++++ 3 files changed, 16 insertions(+), 3 deletions(-) create mode 100644 changelog/6566.bugfix.rst diff --git a/changelog/6566.bugfix.rst b/changelog/6566.bugfix.rst new file mode 100644 index 000000000..4af976f22 --- /dev/null +++ b/changelog/6566.bugfix.rst @@ -0,0 +1 @@ +Fix ``EncodedFile.writelines`` to call the underlying buffer's ``writelines`` method. diff --git a/src/_pytest/capture.py b/src/_pytest/capture.py index c79bfeef0..e51fe2b67 100644 --- a/src/_pytest/capture.py +++ b/src/_pytest/capture.py @@ -9,6 +9,7 @@ import os import sys from io import UnsupportedOperation from tempfile import TemporaryFile +from typing import List import pytest from _pytest.compat import CaptureIO @@ -426,9 +427,8 @@ class EncodedFile: ) return self.buffer.write(obj) - def writelines(self, linelist): - data = "".join(linelist) - self.write(data) + def writelines(self, linelist: List[str]) -> None: + self.buffer.writelines([x.encode(self.encoding, "replace") for x in linelist]) @property def name(self): diff --git a/testing/test_capture.py b/testing/test_capture.py index 7d459e91c..ebe30703b 100644 --- a/testing/test_capture.py +++ b/testing/test_capture.py @@ -1497,3 +1497,15 @@ def test_typeerror_encodedfile_write(testdir): def test_stderr_write_returns_len(capsys): """Write on Encoded files, namely captured stderr, should return number of characters written.""" assert sys.stderr.write("Foo") == 3 + + +def test_encodedfile_writelines(tmpfile) -> None: + ef = capture.EncodedFile(tmpfile, "utf-8") + with pytest.raises(AttributeError): + ef.writelines([b"line1", b"line2"]) # type: ignore[list-item] # noqa: F821 + assert ef.writelines(["line1", "line2"]) is None # type: ignore[func-returns-value] # noqa: F821 + tmpfile.seek(0) + assert tmpfile.read() == b"line1line2" + tmpfile.close() + with pytest.raises(ValueError): + ef.read() From 778d4364fa95e9da504ba82ef7208a8b5d57d720 Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Sat, 25 Jan 2020 18:14:49 +0100 Subject: [PATCH 04/14] tests: test_collection_collect_only_live_logging: allow for 1s Might be slow on CI. Ref: https://github.com/pytest-dev/pytest/pull/6570/checks?check_run_id=408752475#step:6:109 --- testing/logging/test_reporting.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/testing/logging/test_reporting.py b/testing/logging/test_reporting.py index 201f42f32..4333bbb00 100644 --- a/testing/logging/test_reporting.py +++ b/testing/logging/test_reporting.py @@ -946,7 +946,7 @@ def test_collection_collect_only_live_logging(testdir, verbose): expected_lines.extend( [ "*test_collection_collect_only_live_logging.py::test_simple*", - "no tests ran in 0.[0-9][0-9]s", + "no tests ran in [0-1].[0-9][0-9]s", ] ) elif verbose == "-qq": From df1f43ee28d38350542a23acb27647feab46f473 Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Sat, 25 Jan 2020 18:20:46 +0100 Subject: [PATCH 05/14] ci: codecov: use `--retry-connrefused` with curl While it might not help with the following, it certainly might happen as well. ``` + curl -S -L --connect-timeout 5 --retry 6 -s https://codecov.io/bash -o codecov-upload.sh curl: (16) Error in the HTTP2 framing layer ``` --- scripts/report-coverage.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/report-coverage.sh b/scripts/report-coverage.sh index fbcf20ca9..6aa931383 100755 --- a/scripts/report-coverage.sh +++ b/scripts/report-coverage.sh @@ -14,5 +14,5 @@ python -m coverage combine python -m coverage xml python -m coverage report -m # Set --connect-timeout to work around https://github.com/curl/curl/issues/4461 -curl -S -L --connect-timeout 5 --retry 6 -s https://codecov.io/bash -o codecov-upload.sh +curl -S -L --connect-timeout 5 --retry 6 --retry-connrefused -s https://codecov.io/bash -o codecov-upload.sh bash codecov-upload.sh -Z -X fix -f coverage.xml "$@" From 3f8f3952107998f03a7fd1826427a9262a267f6c Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Sat, 25 Jan 2020 18:11:38 +0100 Subject: [PATCH 06/14] typing: EncodedFile --- src/_pytest/capture.py | 15 +++++++-------- testing/test_capture.py | 3 ++- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src/_pytest/capture.py b/src/_pytest/capture.py index e51fe2b67..33d2243b3 100644 --- a/src/_pytest/capture.py +++ b/src/_pytest/capture.py @@ -9,6 +9,7 @@ import os import sys from io import UnsupportedOperation from tempfile import TemporaryFile +from typing import BinaryIO from typing import List import pytest @@ -414,29 +415,27 @@ def safe_text_dupfile(f, mode, default_encoding="UTF8"): class EncodedFile: errors = "strict" # possibly needed by py3 code (issue555) - def __init__(self, buffer, encoding): + def __init__(self, buffer: BinaryIO, encoding: str) -> None: self.buffer = buffer self.encoding = encoding - def write(self, obj): - if isinstance(obj, str): - obj = obj.encode(self.encoding, "replace") - else: + def write(self, obj: str) -> int: + if not isinstance(obj, str): raise TypeError( "write() argument must be str, not {}".format(type(obj).__name__) ) - return self.buffer.write(obj) + return self.buffer.write(obj.encode(self.encoding, "replace")) def writelines(self, linelist: List[str]) -> None: self.buffer.writelines([x.encode(self.encoding, "replace") for x in linelist]) @property - def name(self): + def name(self) -> str: """Ensure that file.name is a string.""" return repr(self.buffer) @property - def mode(self): + def mode(self) -> str: return self.buffer.mode.replace("b", "") def __getattr__(self, name): diff --git a/testing/test_capture.py b/testing/test_capture.py index ebe30703b..e6862f313 100644 --- a/testing/test_capture.py +++ b/testing/test_capture.py @@ -7,6 +7,7 @@ import sys import textwrap from io import StringIO from io import UnsupportedOperation +from typing import BinaryIO from typing import List from typing import TextIO @@ -1499,7 +1500,7 @@ def test_stderr_write_returns_len(capsys): assert sys.stderr.write("Foo") == 3 -def test_encodedfile_writelines(tmpfile) -> None: +def test_encodedfile_writelines(tmpfile: BinaryIO) -> None: ef = capture.EncodedFile(tmpfile, "utf-8") with pytest.raises(AttributeError): ef.writelines([b"line1", b"line2"]) # type: ignore[list-item] # noqa: F821 From d678d380cb407bea0b80e0246752b24e61267478 Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Sat, 25 Jan 2020 19:21:19 +0100 Subject: [PATCH 07/14] typing: tests: tmpfile --- testing/test_capture.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/testing/test_capture.py b/testing/test_capture.py index e6862f313..9261c8441 100644 --- a/testing/test_capture.py +++ b/testing/test_capture.py @@ -8,6 +8,7 @@ import textwrap from io import StringIO from io import UnsupportedOperation from typing import BinaryIO +from typing import Generator from typing import List from typing import TextIO @@ -832,7 +833,7 @@ def test_dontreadfrominput(): @pytest.fixture -def tmpfile(testdir): +def tmpfile(testdir) -> Generator[BinaryIO, None, None]: f = testdir.makepyfile("").open("wb+") yield f if not f.closed: From 40758e86ca9d287069df45c732d586b4905613f6 Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Sun, 26 Jan 2020 16:41:17 +0100 Subject: [PATCH 08/14] tests: add test_via_exec Via https://github.com/pytest-dev/pytest/issues/6574. --- testing/test_terminal.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/testing/test_terminal.py b/testing/test_terminal.py index 09c9d5485..f284ad577 100644 --- a/testing/test_terminal.py +++ b/testing/test_terminal.py @@ -13,6 +13,7 @@ import py import pytest from _pytest.main import ExitCode +from _pytest.pytester import Testdir from _pytest.reports import BaseReport from _pytest.terminal import _folded_skips from _pytest.terminal import _get_line_with_reprcrash_message @@ -1923,3 +1924,11 @@ def test_collecterror(testdir): "*= 1 error in *", ] ) + + +def test_via_exec(testdir: Testdir) -> None: + p1 = testdir.makepyfile("exec('def test_via_exec(): pass')") + result = testdir.runpytest(str(p1), "-vv") + result.stdout.fnmatch_lines( + ["test_via_exec.py::test_via_exec <- PASSED*", "*= 1 passed in *"] + ) From bf5c76359cf9dce7a94989d7f9edd4e22e6ffa3a Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Sun, 26 Jan 2020 23:14:32 +0100 Subject: [PATCH 09/14] fixup! typing: tests: tmpfile --- src/_pytest/capture.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/_pytest/capture.py b/src/_pytest/capture.py index 33d2243b3..ccbeb0884 100644 --- a/src/_pytest/capture.py +++ b/src/_pytest/capture.py @@ -10,7 +10,7 @@ import sys from io import UnsupportedOperation from tempfile import TemporaryFile from typing import BinaryIO -from typing import List +from typing import Iterable import pytest from _pytest.compat import CaptureIO @@ -419,15 +419,15 @@ class EncodedFile: self.buffer = buffer self.encoding = encoding - def write(self, obj: str) -> int: - if not isinstance(obj, str): + def write(self, s: str) -> int: + if not isinstance(s, str): raise TypeError( - "write() argument must be str, not {}".format(type(obj).__name__) + "write() argument must be str, not {}".format(type(s).__name__) ) - return self.buffer.write(obj.encode(self.encoding, "replace")) + return self.buffer.write(s.encode(self.encoding, "replace")) - def writelines(self, linelist: List[str]) -> None: - self.buffer.writelines([x.encode(self.encoding, "replace") for x in linelist]) + def writelines(self, lines: Iterable[str]) -> None: + self.buffer.writelines(x.encode(self.encoding, "replace") for x in lines) @property def name(self) -> str: From d017b69f38e20c9e87f3a8093947230a99dba2ac Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Mon, 27 Jan 2020 22:18:35 +0100 Subject: [PATCH 10/14] mypy: show_error_codes=True --- setup.cfg | 1 + 1 file changed, 1 insertion(+) diff --git a/setup.cfg b/setup.cfg index bef9f7871..708951da4 100644 --- a/setup.cfg +++ b/setup.cfg @@ -65,6 +65,7 @@ formats = sdist.tgz,bdist_wheel mypy_path = src ignore_missing_imports = True no_implicit_optional = True +show_error_codes = True strict_equality = True warn_redundant_casts = True warn_return_any = True From 440881d63a89595b2bb7e67e658e09c72d7ab21d Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Mon, 27 Jan 2020 00:54:20 +0100 Subject: [PATCH 11/14] typing: Testdir.__init__ --- src/_pytest/pytester.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/_pytest/pytester.py b/src/_pytest/pytester.py index 8e7fa5e09..3dfdc3ce4 100644 --- a/src/_pytest/pytester.py +++ b/src/_pytest/pytester.py @@ -35,6 +35,7 @@ from _pytest.main import Session from _pytest.monkeypatch import MonkeyPatch from _pytest.pathlib import Path from _pytest.reports import TestReport +from _pytest.tmpdir import TempdirFactory if TYPE_CHECKING: from typing import Type @@ -534,13 +535,13 @@ class Testdir: class TimeoutExpired(Exception): pass - def __init__(self, request, tmpdir_factory): + def __init__(self, request: FixtureRequest, tmpdir_factory: TempdirFactory) -> None: self.request = request - self._mod_collections = WeakKeyDictionary() + self._mod_collections = WeakKeyDictionary() # type: ignore[var-annotated] # noqa: F821 name = request.function.__name__ self.tmpdir = tmpdir_factory.mktemp(name, numbered=True) self.test_tmproot = tmpdir_factory.mktemp("tmp-" + name, numbered=True) - self.plugins = [] + self.plugins = [] # type: ignore[var-annotated] # noqa: F821 self._cwd_snapshot = CwdSnapshot() self._sys_path_snapshot = SysPathsSnapshot() self._sys_modules_snapshot = self.__take_sys_modules_snapshot() From 94ac0f7e6b27df6675d1295bc5893b337855b3f6 Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Mon, 27 Jan 2020 22:55:42 +0100 Subject: [PATCH 12/14] typing: self._mod_collections, collect_by_name --- src/_pytest/pytester.py | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/src/_pytest/pytester.py b/src/_pytest/pytester.py index 3dfdc3ce4..66f6701a0 100644 --- a/src/_pytest/pytester.py +++ b/src/_pytest/pytester.py @@ -33,7 +33,10 @@ from _pytest.fixtures import FixtureRequest from _pytest.main import ExitCode from _pytest.main import Session from _pytest.monkeypatch import MonkeyPatch +from _pytest.nodes import Collector +from _pytest.nodes import Item from _pytest.pathlib import Path +from _pytest.python import Module from _pytest.reports import TestReport from _pytest.tmpdir import TempdirFactory @@ -537,7 +540,9 @@ class Testdir: def __init__(self, request: FixtureRequest, tmpdir_factory: TempdirFactory) -> None: self.request = request - self._mod_collections = WeakKeyDictionary() # type: ignore[var-annotated] # noqa: F821 + self._mod_collections = ( + WeakKeyDictionary() + ) # type: WeakKeyDictionary[Module, List[Union[Item, Collector]]] name = request.function.__name__ self.tmpdir = tmpdir_factory.mktemp(name, numbered=True) self.test_tmproot = tmpdir_factory.mktemp("tmp-" + name, numbered=True) @@ -1065,7 +1070,9 @@ class Testdir: self.config = config = self.parseconfigure(path, *configargs) return self.getnode(config, path) - def collect_by_name(self, modcol, name): + def collect_by_name( + self, modcol: Module, name: str + ) -> Optional[Union[Item, Collector]]: """Return the collection node for name from the module collection. This will search a module collection node for a collection node @@ -1074,13 +1081,13 @@ class Testdir: :param modcol: a module collection node; see :py:meth:`getmodulecol` :param name: the name of the node to return - """ if modcol not in self._mod_collections: self._mod_collections[modcol] = list(modcol.collect()) for colitem in self._mod_collections[modcol]: if colitem.name == name: return colitem + return None def popen( self, From 9c716e4d747900f80d4dab33185dc20d1f90f859 Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Mon, 27 Jan 2020 22:56:50 +0100 Subject: [PATCH 13/14] typing: Testdir.plugins --- src/_pytest/config/__init__.py | 7 +++++++ src/_pytest/pytester.py | 3 ++- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/src/_pytest/config/__init__.py b/src/_pytest/config/__init__.py index 3516b333e..ed3334e5f 100644 --- a/src/_pytest/config/__init__.py +++ b/src/_pytest/config/__init__.py @@ -48,6 +48,13 @@ if TYPE_CHECKING: from typing import Type +_PluggyPlugin = object +"""A type to represent plugin objects. +Plugins can be any namespace, so we can't narrow it down much, but we use an +alias to make the intent clear. +Ideally this type would be provided by pluggy itself.""" + + hookimpl = HookimplMarker("pytest") hookspec = HookspecMarker("pytest") diff --git a/src/_pytest/pytester.py b/src/_pytest/pytester.py index 66f6701a0..cfe1b9a6c 100644 --- a/src/_pytest/pytester.py +++ b/src/_pytest/pytester.py @@ -29,6 +29,7 @@ from _pytest._io.saferepr import saferepr from _pytest.capture import MultiCapture from _pytest.capture import SysCapture from _pytest.compat import TYPE_CHECKING +from _pytest.config import _PluggyPlugin from _pytest.fixtures import FixtureRequest from _pytest.main import ExitCode from _pytest.main import Session @@ -546,7 +547,7 @@ class Testdir: name = request.function.__name__ self.tmpdir = tmpdir_factory.mktemp(name, numbered=True) self.test_tmproot = tmpdir_factory.mktemp("tmp-" + name, numbered=True) - self.plugins = [] # type: ignore[var-annotated] # noqa: F821 + self.plugins = [] # type: List[Union[str, _PluggyPlugin]] self._cwd_snapshot = CwdSnapshot() self._sys_path_snapshot = SysPathsSnapshot() self._sys_modules_snapshot = self.__take_sys_modules_snapshot() From ad0f4f0ac056e690b4c91c1e04ff47e40110b293 Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Tue, 28 Jan 2020 00:41:27 +0100 Subject: [PATCH 14/14] tests: cover collect_by_name with non-existing --- testing/test_collection.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/testing/test_collection.py b/testing/test_collection.py index 62de0b953..5073f675e 100644 --- a/testing/test_collection.py +++ b/testing/test_collection.py @@ -9,6 +9,7 @@ import pytest from _pytest.main import _in_venv from _pytest.main import ExitCode from _pytest.main import Session +from _pytest.pytester import Testdir class TestCollector: @@ -18,7 +19,7 @@ class TestCollector: assert not issubclass(Collector, Item) assert not issubclass(Item, Collector) - def test_check_equality(self, testdir): + def test_check_equality(self, testdir: Testdir) -> None: modcol = testdir.getmodulecol( """ def test_pass(): pass @@ -40,12 +41,15 @@ class TestCollector: assert fn1 != fn3 for fn in fn1, fn2, fn3: - assert fn != 3 + assert isinstance(fn, pytest.Function) + assert fn != 3 # type: ignore[comparison-overlap] # noqa: F821 assert fn != modcol - assert fn != [1, 2, 3] - assert [1, 2, 3] != fn + assert fn != [1, 2, 3] # type: ignore[comparison-overlap] # noqa: F821 + assert [1, 2, 3] != fn # type: ignore[comparison-overlap] # noqa: F821 assert modcol != fn + assert testdir.collect_by_name(modcol, "doesnotexist") is None + def test_getparent(self, testdir): modcol = testdir.getmodulecol( """