Merge master into features

This commit is contained in:
Daniel Hahler 2020-01-28 01:40:14 +01:00
commit 30922ee694
11 changed files with 94 additions and 25 deletions

View File

@ -0,0 +1 @@
Fix ``EncodedFile.writelines`` to call the underlying buffer's ``writelines`` method.

View File

@ -14,5 +14,5 @@ python -m coverage combine
python -m coverage xml python -m coverage xml
python -m coverage report -m python -m coverage report -m
# Set --connect-timeout to work around https://github.com/curl/curl/issues/4461 # 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 "$@" bash codecov-upload.sh -Z -X fix -f coverage.xml "$@"

View File

@ -10,7 +10,6 @@ project_urls =
author = Holger Krekel, Bruno Oliveira, Ronny Pfannschmidt, Floris Bruynooghe, Brianna Laugher, Florian Bruhin and others author = Holger Krekel, Bruno Oliveira, Ronny Pfannschmidt, Floris Bruynooghe, Brianna Laugher, Florian Bruhin and others
license = MIT license license = MIT license
license_file = LICENSE
keywords = test, unittest keywords = test, unittest
classifiers = classifiers =
Development Status :: 6 - Mature Development Status :: 6 - Mature
@ -66,6 +65,7 @@ formats = sdist.tgz,bdist_wheel
mypy_path = src mypy_path = src
ignore_missing_imports = True ignore_missing_imports = True
no_implicit_optional = True no_implicit_optional = True
show_error_codes = True
strict_equality = True strict_equality = True
warn_redundant_casts = True warn_redundant_casts = True
warn_return_any = True warn_return_any = True

View File

@ -9,6 +9,8 @@ import os
import sys import sys
from io import UnsupportedOperation from io import UnsupportedOperation
from tempfile import TemporaryFile from tempfile import TemporaryFile
from typing import BinaryIO
from typing import Iterable
import pytest import pytest
from _pytest.compat import CaptureAndPassthroughIO from _pytest.compat import CaptureAndPassthroughIO
@ -416,30 +418,27 @@ def safe_text_dupfile(f, mode, default_encoding="UTF8"):
class EncodedFile: class EncodedFile:
errors = "strict" # possibly needed by py3 code (issue555) 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.buffer = buffer
self.encoding = encoding self.encoding = encoding
def write(self, obj): def write(self, s: str) -> int:
if isinstance(obj, str): if not isinstance(s, str):
obj = obj.encode(self.encoding, "replace")
else:
raise TypeError( 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) return self.buffer.write(s.encode(self.encoding, "replace"))
def writelines(self, linelist): def writelines(self, lines: Iterable[str]) -> None:
data = "".join(linelist) self.buffer.writelines(x.encode(self.encoding, "replace") for x in lines)
self.write(data)
@property @property
def name(self): def name(self) -> str:
"""Ensure that file.name is a string.""" """Ensure that file.name is a string."""
return repr(self.buffer) return repr(self.buffer)
@property @property
def mode(self): def mode(self) -> str:
return self.buffer.mode.replace("b", "") return self.buffer.mode.replace("b", "")
def __getattr__(self, name): def __getattr__(self, name):

View File

@ -50,6 +50,13 @@ if TYPE_CHECKING:
from .argparsing import Argument from .argparsing import Argument
_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") hookimpl = HookimplMarker("pytest")
hookspec = HookspecMarker("pytest") hookspec = HookspecMarker("pytest")

View File

@ -29,12 +29,17 @@ from _pytest._io.saferepr import saferepr
from _pytest.capture import MultiCapture from _pytest.capture import MultiCapture
from _pytest.capture import SysCapture from _pytest.capture import SysCapture
from _pytest.compat import TYPE_CHECKING from _pytest.compat import TYPE_CHECKING
from _pytest.config import _PluggyPlugin
from _pytest.fixtures import FixtureRequest from _pytest.fixtures import FixtureRequest
from _pytest.main import ExitCode from _pytest.main import ExitCode
from _pytest.main import Session from _pytest.main import Session
from _pytest.monkeypatch import MonkeyPatch from _pytest.monkeypatch import MonkeyPatch
from _pytest.nodes import Collector
from _pytest.nodes import Item
from _pytest.pathlib import Path from _pytest.pathlib import Path
from _pytest.python import Module
from _pytest.reports import TestReport from _pytest.reports import TestReport
from _pytest.tmpdir import TempdirFactory
if TYPE_CHECKING: if TYPE_CHECKING:
from typing import Type from typing import Type
@ -534,13 +539,15 @@ class Testdir:
class TimeoutExpired(Exception): class TimeoutExpired(Exception):
pass pass
def __init__(self, request, tmpdir_factory): def __init__(self, request: FixtureRequest, tmpdir_factory: TempdirFactory) -> None:
self.request = request self.request = request
self._mod_collections = WeakKeyDictionary() self._mod_collections = (
WeakKeyDictionary()
) # type: WeakKeyDictionary[Module, List[Union[Item, Collector]]]
name = request.function.__name__ name = request.function.__name__
self.tmpdir = tmpdir_factory.mktemp(name, numbered=True) self.tmpdir = tmpdir_factory.mktemp(name, numbered=True)
self.test_tmproot = tmpdir_factory.mktemp("tmp-" + name, numbered=True) self.test_tmproot = tmpdir_factory.mktemp("tmp-" + name, numbered=True)
self.plugins = [] self.plugins = [] # type: List[Union[str, _PluggyPlugin]]
self._cwd_snapshot = CwdSnapshot() self._cwd_snapshot = CwdSnapshot()
self._sys_path_snapshot = SysPathsSnapshot() self._sys_path_snapshot = SysPathsSnapshot()
self._sys_modules_snapshot = self.__take_sys_modules_snapshot() self._sys_modules_snapshot = self.__take_sys_modules_snapshot()
@ -1060,7 +1067,9 @@ class Testdir:
self.config = config = self.parseconfigure(path, *configargs) self.config = config = self.parseconfigure(path, *configargs)
return self.getnode(config, path) 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. """Return the collection node for name from the module collection.
This will search a module collection node for a collection node This will search a module collection node for a collection node
@ -1069,13 +1078,13 @@ class Testdir:
:param modcol: a module collection node; see :py:meth:`getmodulecol` :param modcol: a module collection node; see :py:meth:`getmodulecol`
:param name: the name of the node to return :param name: the name of the node to return
""" """
if modcol not in self._mod_collections: if modcol not in self._mod_collections:
self._mod_collections[modcol] = list(modcol.collect()) self._mod_collections[modcol] = list(modcol.collect())
for colitem in self._mod_collections[modcol]: for colitem in self._mod_collections[modcol]:
if colitem.name == name: if colitem.name == name:
return colitem return colitem
return None
def popen( def popen(
self, self,

View File

@ -946,7 +946,7 @@ def test_collection_collect_only_live_logging(testdir, verbose):
expected_lines.extend( expected_lines.extend(
[ [
"*test_collection_collect_only_live_logging.py::test_simple*", "*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": elif verbose == "-qq":

View File

@ -7,6 +7,8 @@ import sys
import textwrap import textwrap
from io import StringIO from io import StringIO
from io import UnsupportedOperation from io import UnsupportedOperation
from typing import BinaryIO
from typing import Generator
from typing import List from typing import List
from typing import TextIO from typing import TextIO
@ -854,7 +856,7 @@ def test_dontreadfrominput():
@pytest.fixture @pytest.fixture
def tmpfile(testdir): def tmpfile(testdir) -> Generator[BinaryIO, None, None]:
f = testdir.makepyfile("").open("wb+") f = testdir.makepyfile("").open("wb+")
yield f yield f
if not f.closed: if not f.closed:
@ -1537,3 +1539,15 @@ def test_typeerror_encodedfile_write(testdir):
def test_stderr_write_returns_len(capsys): def test_stderr_write_returns_len(capsys):
"""Write on Encoded files, namely captured stderr, should return number of characters written.""" """Write on Encoded files, namely captured stderr, should return number of characters written."""
assert sys.stderr.write("Foo") == 3 assert sys.stderr.write("Foo") == 3
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
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()

View File

@ -9,6 +9,7 @@ import pytest
from _pytest.main import _in_venv from _pytest.main import _in_venv
from _pytest.main import ExitCode from _pytest.main import ExitCode
from _pytest.main import Session from _pytest.main import Session
from _pytest.pytester import Testdir
class TestCollector: class TestCollector:
@ -18,7 +19,7 @@ class TestCollector:
assert not issubclass(Collector, Item) assert not issubclass(Collector, Item)
assert not issubclass(Item, Collector) assert not issubclass(Item, Collector)
def test_check_equality(self, testdir): def test_check_equality(self, testdir: Testdir) -> None:
modcol = testdir.getmodulecol( modcol = testdir.getmodulecol(
""" """
def test_pass(): pass def test_pass(): pass
@ -40,12 +41,15 @@ class TestCollector:
assert fn1 != fn3 assert fn1 != fn3
for fn in fn1, fn2, 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 != modcol
assert fn != [1, 2, 3] assert fn != [1, 2, 3] # type: ignore[comparison-overlap] # noqa: F821
assert [1, 2, 3] != fn assert [1, 2, 3] != fn # type: ignore[comparison-overlap] # noqa: F821
assert modcol != fn assert modcol != fn
assert testdir.collect_by_name(modcol, "doesnotexist") is None
def test_getparent(self, testdir): def test_getparent(self, testdir):
modcol = testdir.getmodulecol( modcol = testdir.getmodulecol(
""" """

View File

@ -670,6 +670,32 @@ def test_disable_plugin_autoload(testdir, monkeypatch, parse_args, should_load):
assert has_loaded == 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): def test_cmdline_processargs_simple(testdir):
testdir.makeconftest( testdir.makeconftest(
""" """

View File

@ -13,6 +13,7 @@ import py
import pytest import pytest
from _pytest.main import ExitCode from _pytest.main import ExitCode
from _pytest.pytester import Testdir
from _pytest.reports import BaseReport from _pytest.reports import BaseReport
from _pytest.terminal import _folded_skips from _pytest.terminal import _folded_skips
from _pytest.terminal import _get_line_with_reprcrash_message from _pytest.terminal import _get_line_with_reprcrash_message
@ -1978,3 +1979,11 @@ def test_collecterror(testdir):
"*= 1 error in *", "*= 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 <- <string> PASSED*", "*= 1 passed in *"]
)