From c9eeafade5079f70d424fc3ba6a55b5b33ceeda1 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Sat, 1 Feb 2020 01:56:45 +0200 Subject: [PATCH 01/20] Fix favicon for Chrome and Opera (#6639) * Fix favicon for Chrome and Opera * Delete pytest1favi.ico Co-authored-by: Bruno Oliveira --- doc/en/conf.py | 2 +- doc/en/img/favicon.png | Bin 0 -> 1334 bytes doc/en/img/pytest1favi.ico | Bin 3742 -> 0 bytes 3 files changed, 1 insertion(+), 1 deletion(-) create mode 100644 doc/en/img/favicon.png delete mode 100644 doc/en/img/pytest1favi.ico diff --git a/doc/en/conf.py b/doc/en/conf.py index bd2fd9871..85521309f 100644 --- a/doc/en/conf.py +++ b/doc/en/conf.py @@ -162,7 +162,7 @@ html_logo = "img/pytest1.png" # The name of an image file (within the static path) to use as favicon of the # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 # pixels large. -html_favicon = "img/pytest1favi.ico" +html_favicon = "img/favicon.png" # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, diff --git a/doc/en/img/favicon.png b/doc/en/img/favicon.png new file mode 100644 index 0000000000000000000000000000000000000000..5c8824d67d34e31baa8623a3d04ef21ee234041e GIT binary patch literal 1334 zcmZ8fdoLuxtft~da-IHza(;D8Wb67)Uid_&a76| z$+o0(nNn-1Y3I^Xm$Pm-oc>d-)7E5d*;&t_>c~E~?sCrazTfSAp6@%)`&A_+CfHiL zSpxuUSxkB|9C0u_UNeQ+Fj6-S2QyI&F9v|l8k_aw=5Xzq!A#}>Q0N6fX*mFEaI5rp z0CEw?NCyC|2f#V|eDAyA0PG1#;_?|_!f1p=0QCFqn^BR#XU6q8FgocASVquzA6%H< z%n;`cw!gJfJnK`o`S-9vL@fAw`#b)l@{aNFg` zwyp>WeOqjCQCh1q?rL86mAnW|7GKR{>mzBp$RK@WP(3rcoQXbEA6m z(QVXaC)Kj!Tfzcb!ja~1q%|zyJR=-@{RlKZ*m11kkFn`pyP$7VzITRvW{Y<1Qjunh ze10anZ9BG|2mEliNG`jEFK${r{VGFhuW?t$4yEQ5jlp34x zoz45_c1VGn|7$B~cs%l(-%g8nd%gW0s1MzPN04YV8rKY%Bm6Yo%H+h9t?#py%xYFx zgyog>t8V(2D}P3^S4vDxTnTu4J3OA?Zi^$}-*}UN$2*%O*QnJR7}eEkjYh4m;TxW% z>9pD=onEW`M*po=tJ5`UbstbUEH2)S$zr1}2iPp;ems}Oe#!lW~9k z-Y-L4jc03)MMozwQ|Wvz_h@WR_e%N51kz_Pr{b9;B!DTG6Rfg#|BHS6A0wSejW_ zSeRFqpQ)%glYk)fct*m=&EZ?#1cDdw;&*zTP9(sD3lAR$QXe`y?sbv}_>(AP@}=+F zTH2eNe`w7T%Q6)bp;$U+XXdVI{3-9L!SL+v^`iW&V@5mAdgQ~T+q^xaykY+k;mCBe z?NrAYPjt@2jP_OMcnq-0=_<|XT1Y?J5SF#EZf?^#N;%voSiCG8e?PtMh?Tuy06#)^ zx^}R27Cf41AM)xh9vbE~yq$IT6P^M`#?_B&c$$x`y7zfIc=dZ*0z97`9nCx90(~Va z8L3LaaitK=$PvN;P!TFZK>{f>giocRfgxy65E-GO2tx6oefb}PJWC+K3Lqgcgc68A e@R3k7_y8J=kP#Xr&{T?fNB}HGBE3B}z3gwU9wCAN literal 0 HcmV?d00001 diff --git a/doc/en/img/pytest1favi.ico b/doc/en/img/pytest1favi.ico deleted file mode 100644 index 6a34fe5c9f7e4a258b0f4ed2cf2efb021dcfbcb2..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3742 zcmeH~dq`7J9LLYpi2mqd^q8)6v1v}Nr1~S~no?=oTofTuk>MYE+`Cu(QASSEw2)}d z6$DXPT5ORT<{$ZnVr$Em@adG=F=}A}6nZv2|;xN%CKDZ->&y%2v=Pb&6Fd^s*4ALkRSKA?y zk?`ID3v3k<@8sX(V?0<|7-Q`gBe1LCTVyLn%%aJH0VpZEaUdD`NfEy$1%reXN@NujG>|jJ8TQdLr3_awt`8-1mAv&m&Q{lI26mC$w@Ybrr(u0X7;*^SuTb}PnP zcwi&eQq0NpY+o)qQ#>c4-W?^sS`sazQ2S_}GJO5+!U^hgr)7)J_aa|%+BY-Hxnum& zEmaa#POW@#&P7=dnVZ~`bSqqw7pQ&mg=(*Cq1q>-cBb^+J+pOyPM$90VXfkNb0q%DKIGpQ(8=>o;qW4VHC`>FBAe)2#0$ii$Ka`fVj Date: Sat, 1 Feb 2020 06:27:41 +0100 Subject: [PATCH 02/20] PyCollector._genfunctions: use already created fixtureinfo (#6636) `Function` creates a `_fixtureinfo` already: https://github.com/pytest-dev/pytest/blob/fed535694/src/_pytest/python.py#L1392-L1395 --- src/_pytest/python.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/_pytest/python.py b/src/_pytest/python.py index 1b94aaf00..65ef1272b 100644 --- a/src/_pytest/python.py +++ b/src/_pytest/python.py @@ -390,7 +390,7 @@ class PyCollector(PyobjMixin, nodes.Collector): fm = self.session._fixturemanager definition = FunctionDefinition(name=name, parent=self, callobj=funcobj) - fixtureinfo = fm.getfixtureinfo(definition, funcobj, cls) + fixtureinfo = definition._fixtureinfo metafunc = Metafunc( definition, fixtureinfo, self.config, cls=cls, module=module From 8bd612b36734972d54bdf3f9c27cd69919372927 Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Sun, 2 Feb 2020 22:50:30 +0100 Subject: [PATCH 03/20] typing: wrap_session Pulled out of https://github.com/pytest-dev/pytest/pull/6556. --- src/_pytest/main.py | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/src/_pytest/main.py b/src/_pytest/main.py index e5666da9f..8ef06db38 100644 --- a/src/_pytest/main.py +++ b/src/_pytest/main.py @@ -5,6 +5,7 @@ import functools import importlib import os import sys +from typing import Callable from typing import Dict from typing import FrozenSet from typing import List @@ -23,7 +24,7 @@ from _pytest.config import hookimpl from _pytest.config import UsageError from _pytest.fixtures import FixtureManager from _pytest.nodes import Node -from _pytest.outcomes import exit +from _pytest.outcomes import Exit from _pytest.runner import collect_one_node from _pytest.runner import SetupState @@ -194,7 +195,9 @@ def pytest_addoption(parser): ) -def wrap_session(config, doit): +def wrap_session( + config: Config, doit: Callable[[Config, "Session"], Optional[Union[int, ExitCode]]] +) -> Union[int, ExitCode]: """Skeleton command line program""" session = Session(config) session.exitstatus = ExitCode.OK @@ -211,10 +214,10 @@ def wrap_session(config, doit): raise except Failed: session.exitstatus = ExitCode.TESTS_FAILED - except (KeyboardInterrupt, exit.Exception): + except (KeyboardInterrupt, Exit): excinfo = _pytest._code.ExceptionInfo.from_current() - exitstatus = ExitCode.INTERRUPTED - if isinstance(excinfo.value, exit.Exception): + exitstatus = ExitCode.INTERRUPTED # type: Union[int, ExitCode] + if isinstance(excinfo.value, Exit): if excinfo.value.returncode is not None: exitstatus = excinfo.value.returncode if initstate < 2: @@ -228,7 +231,7 @@ def wrap_session(config, doit): excinfo = _pytest._code.ExceptionInfo.from_current() try: config.notify_exception(excinfo, config.option) - except exit.Exception as exc: + except Exit as exc: if exc.returncode is not None: session.exitstatus = exc.returncode sys.stderr.write("{}: {}\n".format(type(exc).__name__, exc)) @@ -237,7 +240,8 @@ def wrap_session(config, doit): sys.stderr.write("mainloop: caught unexpected SystemExit!\n") finally: - excinfo = None # Explicitly break reference cycle. + # Explicitly break reference cycle. + excinfo = None # type: ignore session.startdir.chdir() if initstate >= 2: config.hook.pytest_sessionfinish( @@ -382,6 +386,7 @@ class Session(nodes.FSCollector): _setupstate = None # type: SetupState # Set on the session by fixtures.pytest_sessionstart. _fixturemanager = None # type: FixtureManager + exitstatus = None # type: Union[int, ExitCode] def __init__(self, config: Config) -> None: nodes.FSCollector.__init__( From 99d162e44a0d40675b855dbcde9734b29032f8aa Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Sun, 2 Feb 2020 22:23:41 +0100 Subject: [PATCH 04/20] Handle `Exit` exception in `pytest_sessionfinish` Similar to a7268aa (https://github.com/pytest-dev/pytest/pull/6258). --- changelog/6660.bugfix.rst | 1 + src/_pytest/main.py | 11 ++++++++--- testing/test_main.py | 25 +++++++++++++++++++++++++ 3 files changed, 34 insertions(+), 3 deletions(-) create mode 100644 changelog/6660.bugfix.rst diff --git a/changelog/6660.bugfix.rst b/changelog/6660.bugfix.rst new file mode 100644 index 000000000..bcc2e1d94 --- /dev/null +++ b/changelog/6660.bugfix.rst @@ -0,0 +1 @@ +:func:`pytest.exit() <_pytest.outcomes.exit>` is handled when emitted from the :func:`pytest_sessionfinish <_pytest.hookspec.pytest_sessionfinish>` hook. This includes quitting from a debugger. diff --git a/src/_pytest/main.py b/src/_pytest/main.py index 8ef06db38..59c3c6714 100644 --- a/src/_pytest/main.py +++ b/src/_pytest/main.py @@ -244,9 +244,14 @@ def wrap_session( excinfo = None # type: ignore session.startdir.chdir() if initstate >= 2: - config.hook.pytest_sessionfinish( - session=session, exitstatus=session.exitstatus - ) + try: + config.hook.pytest_sessionfinish( + session=session, exitstatus=session.exitstatus + ) + except Exit as exc: + if exc.returncode is not None: + session.exitstatus = exc.returncode + sys.stderr.write("{}: {}\n".format(type(exc).__name__, exc)) config._ensure_unconfigure() return session.exitstatus diff --git a/testing/test_main.py b/testing/test_main.py index b47791b29..49e3decd0 100644 --- a/testing/test_main.py +++ b/testing/test_main.py @@ -1,5 +1,8 @@ +from typing import Optional + import pytest from _pytest.main import ExitCode +from _pytest.pytester import Testdir @pytest.mark.parametrize( @@ -50,3 +53,25 @@ def test_wrap_session_notify_exception(ret_exc, testdir): assert result.stderr.lines == ["mainloop: caught unexpected SystemExit!"] else: assert result.stderr.lines == ["Exit: exiting after {}...".format(exc.__name__)] + + +@pytest.mark.parametrize("returncode", (None, 42)) +def test_wrap_session_exit_sessionfinish( + returncode: Optional[int], testdir: Testdir +) -> None: + testdir.makeconftest( + """ + import pytest + def pytest_sessionfinish(): + pytest.exit(msg="exit_pytest_sessionfinish", returncode={returncode}) + """.format( + returncode=returncode + ) + ) + result = testdir.runpytest() + if returncode: + assert result.ret == returncode + else: + assert result.ret == ExitCode.NO_TESTS_COLLECTED + assert result.stdout.lines[-1] == "collected 0 items" + assert result.stderr.lines == ["Exit: exit_pytest_sessionfinish"] From c55bf23cbeb17df0621a9da2a15cff06a9792de8 Mon Sep 17 00:00:00 2001 From: rebecca-palmer Date: Mon, 3 Feb 2020 07:56:37 +0000 Subject: [PATCH 05/20] doc: s/pytest_mark/pytestmark (#6661) --- doc/en/reference.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/doc/en/reference.rst b/doc/en/reference.rst index 50e32d660..088f6a065 100644 --- a/doc/en/reference.rst +++ b/doc/en/reference.rst @@ -901,8 +901,8 @@ Can be either a ``str`` or ``Sequence[str]``. pytest_plugins = ("myapp.testsupport.tools", "myapp.testsupport.regression") -pytest_mark -~~~~~~~~~~~ +pytestmark +~~~~~~~~~~ **Tutorial**: :ref:`scoped-marking` From fb289667e32d5837b1d15e34b2909ee74f876960 Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Mon, 3 Feb 2020 13:53:31 +0100 Subject: [PATCH 06/20] Remove testing/test_modimport.py testing/test_meta.py ensures this already as a side effect (+ tests a few more (`__init__.py` files) and should have been combined with it right away [1]. 1: https://github.com/pytest-dev/pytest/pull/4510#discussion_r289123446 Ref: https://github.com/pytest-dev/pytest/commit/eaa05531e Ref: https://github.com/pytest-dev/pytest/commit/4d31ea831 --- testing/test_modimport.py | 40 --------------------------------------- 1 file changed, 40 deletions(-) delete mode 100644 testing/test_modimport.py diff --git a/testing/test_modimport.py b/testing/test_modimport.py deleted file mode 100644 index 3d7a07323..000000000 --- a/testing/test_modimport.py +++ /dev/null @@ -1,40 +0,0 @@ -import subprocess -import sys - -import py - -import _pytest -import pytest - -pytestmark = pytest.mark.slow - -MODSET = [ - x - for x in py.path.local(_pytest.__file__).dirpath().visit("*.py") - if x.purebasename != "__init__" -] - - -@pytest.mark.parametrize("modfile", MODSET, ids=lambda x: x.purebasename) -def test_fileimport(modfile): - # this test ensures all internal packages can import - # without needing the pytest namespace being set - # this is critical for the initialization of xdist - - p = subprocess.Popen( - [ - sys.executable, - "-c", - "import sys, py; py.path.local(sys.argv[1]).pyimport()", - modfile.strpath, - ], - stdout=subprocess.PIPE, - stderr=subprocess.PIPE, - ) - (out, err) = p.communicate() - assert p.returncode == 0, "importing %s failed (exitcode %d): out=%r, err=%r" % ( - modfile, - p.returncode, - out, - err, - ) From abffd16ce6e950a27b013f017b0bee167f095bf8 Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Mon, 3 Feb 2020 14:04:16 +0100 Subject: [PATCH 07/20] Keep (revisited) comment from https://github.com/pytest-dev/pytest/commit/4d31ea831 --- testing/test_meta.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/testing/test_meta.py b/testing/test_meta.py index 296aa42aa..ffc8fd38a 100644 --- a/testing/test_meta.py +++ b/testing/test_meta.py @@ -1,3 +1,9 @@ +""" +Test importing of all internal packages and modules. + +This ensures all internal packages can be imported without needing the pytest +namespace being set, which is critical for the initialization of xdist. +""" import pkgutil import subprocess import sys From 1480aa31a76feef504f392e52f5730ee12476988 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Mon, 3 Feb 2020 14:35:50 -0300 Subject: [PATCH 08/20] Explicitly state on the PR template that we can squash commits (#6662) * Explicitly state on the PR template that we can squash commits This way we don't need to ask every time, and users who for some reason would not like us to squash their commits can explicitly state so. --- .github/PULL_REQUEST_TEMPLATE.md | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 7f9aa9556..2e221f73e 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -7,6 +7,7 @@ Here is a quick checklist that should be present in PRs. - [ ] Target the `features` branch for new features, improvements, and removals/deprecations. - [ ] Include documentation when adding new features. - [ ] Include new tests or update existing tests when applicable. +- [X] Allow maintainers to push and squash when merging my commits. Please uncheck this if you prefer to squash the commits yourself. Unless your change is trivial or a small documentation fix (e.g., a typo or reword of a small section) please: From b0d45267c58859bcb79f7ab980f4b410c4bbd109 Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Sun, 2 Feb 2020 03:42:53 +0100 Subject: [PATCH 09/20] internal: clean up getfslineno Everything was using `_pytest.compat.getfslineno` basically, which wrapped `_pytest._code.source.getfslineno`. This moves the extra code from there into it directly, and uses the latter everywhere. This helps to eventually remove the one in compat eventually, and also causes less cyclic imports. --- src/_pytest/_code/source.py | 8 ++++++++ src/_pytest/compat.py | 12 ++++-------- src/_pytest/fixtures.py | 2 +- src/_pytest/mark/structures.py | 2 +- src/_pytest/nodes.py | 2 +- src/_pytest/python.py | 2 +- 6 files changed, 16 insertions(+), 12 deletions(-) diff --git a/src/_pytest/_code/source.py b/src/_pytest/_code/source.py index 379393b10..b5e18863f 100644 --- a/src/_pytest/_code/source.py +++ b/src/_pytest/_code/source.py @@ -17,6 +17,7 @@ from typing import Union import py +from _pytest.compat import get_real_func from _pytest.compat import overload from _pytest.compat import TYPE_CHECKING @@ -290,6 +291,13 @@ def getfslineno(obj) -> Tuple[Optional[Union["Literal['']", py.path.local]], int """ from .code import Code + # xxx let decorators etc specify a sane ordering + # NOTE: this used to be done in _pytest.compat.getfslineno, initially added + # in 6ec13a2b9. It ("place_as") appears to be something very custom. + obj = get_real_func(obj) + if hasattr(obj, "place_as"): + obj = obj.place_as + try: code = Code(obj) except TypeError: diff --git a/src/_pytest/compat.py b/src/_pytest/compat.py index 085f634a4..d6ee1d522 100644 --- a/src/_pytest/compat.py +++ b/src/_pytest/compat.py @@ -22,7 +22,6 @@ from typing import Union import attr import py -import _pytest from _pytest._io.saferepr import saferepr from _pytest.outcomes import fail from _pytest.outcomes import TEST_OUTCOME @@ -308,13 +307,10 @@ def get_real_method(obj, holder): def getfslineno(obj) -> Tuple[Union[str, py.path.local], int]: - # xxx let decorators etc specify a sane ordering - obj = get_real_func(obj) - if hasattr(obj, "place_as"): - obj = obj.place_as - fslineno = _pytest._code.getfslineno(obj) - assert isinstance(fslineno[1], int), obj - return fslineno + """(**Deprecated**, use _pytest._code.source.getfslineno directly)""" + from _pytest._code.source import getfslineno + + return getfslineno(obj) def getimfunc(func): diff --git a/src/_pytest/fixtures.py b/src/_pytest/fixtures.py index 5b3686b58..a6bfeb6d3 100644 --- a/src/_pytest/fixtures.py +++ b/src/_pytest/fixtures.py @@ -16,12 +16,12 @@ import py import _pytest from _pytest._code.code import FormattedExcinfo from _pytest._code.code import TerminalRepr +from _pytest._code.source import getfslineno from _pytest._io import TerminalWriter from _pytest.compat import _format_args from _pytest.compat import _PytestWrapper from _pytest.compat import get_real_func from _pytest.compat import get_real_method -from _pytest.compat import getfslineno from _pytest.compat import getfuncargnames from _pytest.compat import getimfunc from _pytest.compat import getlocation diff --git a/src/_pytest/mark/structures.py b/src/_pytest/mark/structures.py index 3002f8abc..de4333a62 100644 --- a/src/_pytest/mark/structures.py +++ b/src/_pytest/mark/structures.py @@ -6,9 +6,9 @@ from typing import Set import attr +from .._code.source import getfslineno from ..compat import ascii_escaped from ..compat import ATTRS_EQ_FIELD -from ..compat import getfslineno from ..compat import NOTSET from _pytest.outcomes import fail from _pytest.warning_types import PytestUnknownMarkWarning diff --git a/src/_pytest/nodes.py b/src/_pytest/nodes.py index 5447f2541..218684e14 100644 --- a/src/_pytest/nodes.py +++ b/src/_pytest/nodes.py @@ -15,8 +15,8 @@ import _pytest._code from _pytest._code.code import ExceptionChainRepr from _pytest._code.code import ExceptionInfo from _pytest._code.code import ReprExceptionInfo +from _pytest._code.source import getfslineno from _pytest.compat import cached_property -from _pytest.compat import getfslineno from _pytest.compat import TYPE_CHECKING from _pytest.config import Config from _pytest.config import PytestPluginManager diff --git a/src/_pytest/python.py b/src/_pytest/python.py index 65ef1272b..525498de2 100644 --- a/src/_pytest/python.py +++ b/src/_pytest/python.py @@ -20,10 +20,10 @@ import _pytest from _pytest import fixtures from _pytest import nodes from _pytest._code import filter_traceback +from _pytest._code.source import getfslineno from _pytest.compat import ascii_escaped from _pytest.compat import get_default_arg_names from _pytest.compat import get_real_func -from _pytest.compat import getfslineno from _pytest.compat import getimfunc from _pytest.compat import getlocation from _pytest.compat import is_generator From 61f2a26675561d510ab4f736a5b3c5d4f8aa043c Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Mon, 3 Feb 2020 18:40:23 +0100 Subject: [PATCH 10/20] Code/getfslineno: keep empty co_filename Previously this would be turned via `py.path.local("")` into the current working directory. This appears to be what `fspath = fn and py.path.local(fn) or None` tries to avoid in `getfslineno`'s `TypeError` handling already, if `Code` would raise it. --- src/_pytest/_code/code.py | 2 ++ testing/code/test_source.py | 8 ++++++++ 2 files changed, 10 insertions(+) diff --git a/src/_pytest/_code/code.py b/src/_pytest/_code/code.py index b176dde98..cafd870f0 100644 --- a/src/_pytest/_code/code.py +++ b/src/_pytest/_code/code.py @@ -72,6 +72,8 @@ class Code: """ return a path object pointing to source code (or a str in case of OSError / non-existing file). """ + if not self.raw.co_filename: + return "" try: p = py.path.local(self.raw.co_filename) # maybe don't try this checking diff --git a/testing/code/test_source.py b/testing/code/test_source.py index b5efdb317..cf0930974 100644 --- a/testing/code/test_source.py +++ b/testing/code/test_source.py @@ -524,6 +524,14 @@ def test_getfslineno() -> None: B.__name__ = "B2" assert getfslineno(B)[1] == -1 + co = compile("...", "", "eval") + assert co.co_filename == "" + + if hasattr(sys, "pypy_version_info"): + assert getfslineno(co) == ("", -1) + else: + assert getfslineno(co) == ("", 0) + def test_code_of_object_instance_with_call() -> None: class A: From dab90ef726cf33579e692820f82797d8e906ff8a Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Mon, 3 Feb 2020 18:50:12 +0100 Subject: [PATCH 11/20] typing: fix getfslineno Closes https://github.com/pytest-dev/pytest/pull/6590. --- src/_pytest/_code/source.py | 11 +++++------ src/_pytest/compat.py | 4 ++-- 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/src/_pytest/_code/source.py b/src/_pytest/_code/source.py index b5e18863f..432e1cbe8 100644 --- a/src/_pytest/_code/source.py +++ b/src/_pytest/_code/source.py @@ -8,6 +8,7 @@ import warnings from bisect import bisect_right from types import CodeType from types import FrameType +from typing import Any from typing import Iterator from typing import List from typing import Optional @@ -283,7 +284,7 @@ def compile_( # noqa: F811 return s.compile(filename, mode, flags, _genframe=_genframe) -def getfslineno(obj) -> Tuple[Optional[Union["Literal['']", py.path.local]], int]: +def getfslineno(obj: Any) -> Tuple[Union[str, py.path.local], int]: """ Return source location (path, lineno) for the given object. If the source cannot be determined return ("", -1). @@ -306,18 +307,16 @@ def getfslineno(obj) -> Tuple[Optional[Union["Literal['']", py.path.local]], int except TypeError: return "", -1 - fspath = fn and py.path.local(fn) or None + fspath = fn and py.path.local(fn) or "" lineno = -1 if fspath: try: _, lineno = findsource(obj) except IOError: pass + return fspath, lineno else: - fspath = code.path - lineno = code.firstlineno - assert isinstance(lineno, int) - return fspath, lineno + return code.path, code.firstlineno # diff --git a/src/_pytest/compat.py b/src/_pytest/compat.py index d6ee1d522..3a3645c5a 100644 --- a/src/_pytest/compat.py +++ b/src/_pytest/compat.py @@ -308,9 +308,9 @@ def get_real_method(obj, holder): def getfslineno(obj) -> Tuple[Union[str, py.path.local], int]: """(**Deprecated**, use _pytest._code.source.getfslineno directly)""" - from _pytest._code.source import getfslineno + import _pytest._code.source - return getfslineno(obj) + return _pytest._code.source.getfslineno(obj) def getimfunc(func): From 9c7f1d9b329f97914d75c2891f20def973429fa5 Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Tue, 4 Feb 2020 02:40:59 +0100 Subject: [PATCH 12/20] Remove compat.getfslineno --- src/_pytest/compat.py | 7 ------- 1 file changed, 7 deletions(-) diff --git a/src/_pytest/compat.py b/src/_pytest/compat.py index 3a3645c5a..f204dbd2d 100644 --- a/src/_pytest/compat.py +++ b/src/_pytest/compat.py @@ -306,13 +306,6 @@ def get_real_method(obj, holder): return obj -def getfslineno(obj) -> Tuple[Union[str, py.path.local], int]: - """(**Deprecated**, use _pytest._code.source.getfslineno directly)""" - import _pytest._code.source - - return _pytest._code.source.getfslineno(obj) - - def getimfunc(func): try: return func.__func__ From aa0328782f9c92d7497ff28f77972afe3cb5b8e2 Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Tue, 4 Feb 2020 02:56:23 +0100 Subject: [PATCH 13/20] assertion: save/restore hooks on item (#6646) --- changelog/6646.bugfix.rst | 1 + src/_pytest/assertion/__init__.py | 10 ++++++---- src/_pytest/config/__init__.py | 5 ++++- testing/test_assertion.py | 17 +++++++++++++---- 4 files changed, 24 insertions(+), 9 deletions(-) create mode 100644 changelog/6646.bugfix.rst diff --git a/changelog/6646.bugfix.rst b/changelog/6646.bugfix.rst new file mode 100644 index 000000000..4dba3ed07 --- /dev/null +++ b/changelog/6646.bugfix.rst @@ -0,0 +1 @@ +Assertion rewriting hooks are (re)stored for the current item, which fixes them being still used after e.g. pytester's :func:`testdir.runpytest <_pytest.pytester.Testdir.runpytest>` etc. diff --git a/src/_pytest/assertion/__init__.py b/src/_pytest/assertion/__init__.py index a060723a7..cdb034703 100644 --- a/src/_pytest/assertion/__init__.py +++ b/src/_pytest/assertion/__init__.py @@ -8,6 +8,7 @@ from _pytest.assertion import rewrite from _pytest.assertion import truncate from _pytest.assertion import util from _pytest.compat import TYPE_CHECKING +from _pytest.config import hookimpl if TYPE_CHECKING: from _pytest.main import Session @@ -105,7 +106,8 @@ def pytest_collection(session: "Session") -> None: assertstate.hook.set_session(session) -def pytest_runtest_setup(item): +@hookimpl(tryfirst=True, hookwrapper=True) +def pytest_runtest_protocol(item): """Setup the pytest_assertrepr_compare and pytest_assertion_pass hooks The newinterpret and rewrite modules will use util._reprcompare if @@ -143,6 +145,7 @@ def pytest_runtest_setup(item): return res return None + saved_assert_hooks = util._reprcompare, util._assertion_pass util._reprcompare = callbinrepr if item.ihook.pytest_assertion_pass.get_hookimpls(): @@ -154,10 +157,9 @@ def pytest_runtest_setup(item): util._assertion_pass = call_assertion_pass_hook + yield -def pytest_runtest_teardown(item): - util._reprcompare = None - util._assertion_pass = None + util._reprcompare, util._assertion_pass = saved_assert_hooks def pytest_sessionfinish(session): diff --git a/src/_pytest/config/__init__.py b/src/_pytest/config/__init__.py index ed3334e5f..d4477ba81 100644 --- a/src/_pytest/config/__init__.py +++ b/src/_pytest/config/__init__.py @@ -27,7 +27,6 @@ from pluggy import HookspecMarker from pluggy import PluginManager import _pytest._code -import _pytest.assertion import _pytest.deprecated import _pytest.hookspec # the extension point definitions from .exceptions import PrintHelp @@ -260,6 +259,8 @@ class PytestPluginManager(PluginManager): """ def __init__(self): + import _pytest.assertion + super().__init__("pytest") # The objects are module objects, only used generically. self._conftest_plugins = set() # type: Set[object] @@ -891,6 +892,8 @@ class Config: ns, unknown_args = self._parser.parse_known_and_unknown_args(args) mode = getattr(ns, "assertmode", "plain") if mode == "rewrite": + import _pytest.assertion + try: hook = _pytest.assertion.install_importhook(self) except SystemError: diff --git a/testing/test_assertion.py b/testing/test_assertion.py index e975a3fea..dc260b39f 100644 --- a/testing/test_assertion.py +++ b/testing/test_assertion.py @@ -72,10 +72,19 @@ class TestImportHookInstallation: result = testdir.runpytest_subprocess() result.stdout.fnmatch_lines( [ - "E * AssertionError: ([[][]], [[][]], [[][]])*", - "E * assert" - " {'failed': 1, 'passed': 0, 'skipped': 0} ==" - " {'failed': 0, 'passed': 1, 'skipped': 0}", + "> r.assertoutcome(passed=1)", + "E AssertionError: ([[][]], [[][]], [[][]])*", + "E assert {'failed': 1,... 'skipped': 0} == {'failed': 0,... 'skipped': 0}", + "E Omitting 1 identical items, use -vv to show", + "E Differing items:", + "E Use -v to get the full diff", + ] + ) + # XXX: unstable output. + result.stdout.fnmatch_lines_random( + [ + "E {'failed': 1} != {'failed': 0}", + "E {'passed': 0} != {'passed': 1}", ] ) From 4316fe8a92ce457b897043c32bb49243858e9960 Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Tue, 4 Feb 2020 02:59:20 +0100 Subject: [PATCH 14/20] testing/conftest.py: testdir: set PYTEST_DISABLE_PLUGIN_AUTOLOAD=1 (#6655) Fixes https://github.com/pytest-dev/pytest/pull/4518. --- testing/acceptance_test.py | 2 ++ testing/conftest.py | 7 +++++++ testing/test_helpconfig.py | 1 + testing/test_junitxml.py | 1 + testing/test_terminal.py | 2 ++ 5 files changed, 13 insertions(+) diff --git a/testing/acceptance_test.py b/testing/acceptance_test.py index f65a60b44..9bc7367c8 100644 --- a/testing/acceptance_test.py +++ b/testing/acceptance_test.py @@ -104,6 +104,8 @@ class TestGeneralUsage: @pytest.mark.parametrize("load_cov_early", [True, False]) def test_early_load_setuptools_name(self, testdir, monkeypatch, load_cov_early): + monkeypatch.delenv("PYTEST_DISABLE_PLUGIN_AUTOLOAD") + testdir.makepyfile(mytestplugin1_module="") testdir.makepyfile(mytestplugin2_module="") testdir.makepyfile(mycov_module="") diff --git a/testing/conftest.py b/testing/conftest.py index 33b817a12..3127fda6a 100644 --- a/testing/conftest.py +++ b/testing/conftest.py @@ -1,6 +1,7 @@ import sys import pytest +from _pytest.pytester import Testdir if sys.gettrace(): @@ -118,3 +119,9 @@ def dummy_yaml_custom_test(testdir): """ ) testdir.makefile(".yaml", test1="") + + +@pytest.fixture +def testdir(testdir: Testdir) -> Testdir: + testdir.monkeypatch.setenv("PYTEST_DISABLE_PLUGIN_AUTOLOAD", "1") + return testdir diff --git a/testing/test_helpconfig.py b/testing/test_helpconfig.py index 1dee5b0f5..a06ba0e26 100644 --- a/testing/test_helpconfig.py +++ b/testing/test_helpconfig.py @@ -3,6 +3,7 @@ from _pytest.main import ExitCode def test_version(testdir, pytestconfig): + testdir.monkeypatch.delenv("PYTEST_DISABLE_PLUGIN_AUTOLOAD") result = testdir.runpytest("--version") assert result.ret == 0 # p = py.path.local(py.__file__).dirpath() diff --git a/testing/test_junitxml.py b/testing/test_junitxml.py index 365332d70..6532a89b1 100644 --- a/testing/test_junitxml.py +++ b/testing/test_junitxml.py @@ -1227,6 +1227,7 @@ def test_runs_twice(testdir, run_and_parse): def test_runs_twice_xdist(testdir, run_and_parse): pytest.importorskip("xdist") + testdir.monkeypatch.delenv("PYTEST_DISABLE_PLUGIN_AUTOLOAD") f = testdir.makepyfile( """ def test_pass(): diff --git a/testing/test_terminal.py b/testing/test_terminal.py index c3a0c17e1..cc2f6d5fb 100644 --- a/testing/test_terminal.py +++ b/testing/test_terminal.py @@ -604,6 +604,7 @@ class TestTerminalFunctional: assert result.ret == 0 def test_header_trailer_info(self, testdir, request): + testdir.monkeypatch.delenv("PYTEST_DISABLE_PLUGIN_AUTOLOAD") testdir.makepyfile( """ def test_passes(): @@ -714,6 +715,7 @@ class TestTerminalFunctional: if not pytestconfig.pluginmanager.get_plugin("xdist"): pytest.skip("xdist plugin not installed") + testdir.monkeypatch.delenv("PYTEST_DISABLE_PLUGIN_AUTOLOAD") result = testdir.runpytest( verbose_testfile, "-v", "-n 1", "-Walways::pytest.PytestWarning" ) From cdc7e130679c35fbb54bcff033a2b7b2d8ff3029 Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Wed, 5 Feb 2020 20:42:57 +0100 Subject: [PATCH 15/20] pytester: clarify _makefile signature (#6675) --- src/_pytest/pytester.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/_pytest/pytester.py b/src/_pytest/pytester.py index cfe1b9a6c..60088502e 100644 --- a/src/_pytest/pytester.py +++ b/src/_pytest/pytester.py @@ -608,14 +608,14 @@ class Testdir: """ self.tmpdir.chdir() - def _makefile(self, ext, args, kwargs, encoding="utf-8"): - items = list(kwargs.items()) + def _makefile(self, ext, lines, files, encoding="utf-8"): + items = list(files.items()) def to_text(s): return s.decode(encoding) if isinstance(s, bytes) else str(s) - if args: - source = "\n".join(to_text(x) for x in args) + if lines: + source = "\n".join(to_text(x) for x in lines) basename = self.request.function.__name__ items.insert(0, (basename, source)) From ef437ea44831c949650376c24f925a023f4192db Mon Sep 17 00:00:00 2001 From: Minuddin Ahmed Rana Date: Thu, 6 Feb 2020 01:45:21 +0600 Subject: [PATCH 16/20] Remove incorrect choices comment (#6677) --- src/_pytest/junitxml.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/_pytest/junitxml.py b/src/_pytest/junitxml.py index 206e44d96..c99c79f10 100644 --- a/src/_pytest/junitxml.py +++ b/src/_pytest/junitxml.py @@ -410,7 +410,7 @@ def pytest_addoption(parser): "Write captured log messages to JUnit report: " "one of no|system-out|system-err", default="no", - ) # choices=['no', 'stdout', 'stderr']) + ) parser.addini( "junit_log_passing_tests", "Capture log information for passing tests to JUnit report: ", From 30cb598e9c4bda0d35aeea6657ba6a957bcec957 Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Sun, 9 Feb 2020 11:42:07 +0100 Subject: [PATCH 17/20] Typing around/from types in docs (#6699) --- src/_pytest/logging.py | 23 +++++++++++++---------- src/_pytest/mark/structures.py | 7 +++++-- src/_pytest/nodes.py | 11 +++++++---- src/_pytest/python.py | 9 ++++++--- 4 files changed, 31 insertions(+), 19 deletions(-) diff --git a/src/_pytest/logging.py b/src/_pytest/logging.py index df0da3daa..3fccee005 100644 --- a/src/_pytest/logging.py +++ b/src/_pytest/logging.py @@ -10,6 +10,7 @@ from typing import List from typing import Mapping import pytest +from _pytest import nodes from _pytest.compat import nullcontext from _pytest.config import _strtobool from _pytest.config import create_terminal_writer @@ -326,13 +327,13 @@ class LogCaptureFixture: logger.setLevel(level) @property - def handler(self): + def handler(self) -> LogCaptureHandler: """ :rtype: LogCaptureHandler """ - return self._item.catch_log_handler + return self._item.catch_log_handler # type: ignore[no-any-return] # noqa: F723 - def get_records(self, when): + def get_records(self, when: str) -> List[logging.LogRecord]: """ Get the logging records for one of the possible test phases. @@ -346,7 +347,7 @@ class LogCaptureFixture: """ handler = self._item.catch_log_handlers.get(when) if handler: - return handler.records + return handler.records # type: ignore[no-any-return] # noqa: F723 else: return [] @@ -613,7 +614,9 @@ class LoggingPlugin: yield @contextmanager - def _runtest_for_main(self, item, when): + def _runtest_for_main( + self, item: nodes.Item, when: str + ) -> Generator[None, None, None]: """Implements the internals of pytest_runtest_xxx() hook.""" with catching_logs( LogCaptureHandler(), formatter=self.formatter, level=self.log_level @@ -626,15 +629,15 @@ class LoggingPlugin: return if not hasattr(item, "catch_log_handlers"): - item.catch_log_handlers = {} - item.catch_log_handlers[when] = log_handler - item.catch_log_handler = log_handler + item.catch_log_handlers = {} # type: ignore[attr-defined] # noqa: F821 + item.catch_log_handlers[when] = log_handler # type: ignore[attr-defined] # noqa: F821 + item.catch_log_handler = log_handler # type: ignore[attr-defined] # noqa: F821 try: yield # run test finally: if when == "teardown": - del item.catch_log_handler - del item.catch_log_handlers + del item.catch_log_handler # type: ignore[attr-defined] # noqa: F821 + del item.catch_log_handlers # type: ignore[attr-defined] # noqa: F821 if self.print_logs: # Add a captured log section to the report. diff --git a/src/_pytest/mark/structures.py b/src/_pytest/mark/structures.py index de4333a62..161f623ee 100644 --- a/src/_pytest/mark/structures.py +++ b/src/_pytest/mark/structures.py @@ -2,7 +2,10 @@ import inspect import warnings from collections import namedtuple from collections.abc import MutableMapping +from typing import Iterable +from typing import List from typing import Set +from typing import Union import attr @@ -144,7 +147,7 @@ class Mark: #: keyword arguments of the mark decorator kwargs = attr.ib() # Dict[str, object] - def combined_with(self, other): + def combined_with(self, other: "Mark") -> "Mark": """ :param other: the mark to combine with :type other: Mark @@ -249,7 +252,7 @@ def get_unpacked_marks(obj): return normalize_mark_list(mark_list) -def normalize_mark_list(mark_list): +def normalize_mark_list(mark_list: Iterable[Union[Mark, MarkDecorator]]) -> List[Mark]: """ normalizes marker decorating helpers to mark objects diff --git a/src/_pytest/nodes.py b/src/_pytest/nodes.py index 218684e14..641f889fe 100644 --- a/src/_pytest/nodes.py +++ b/src/_pytest/nodes.py @@ -333,7 +333,9 @@ class Node: return self._repr_failure_py(excinfo, style) -def get_fslocation_from_item(item): +def get_fslocation_from_item( + item: "Item", +) -> Tuple[Union[str, py.path.local], Optional[int]]: """Tries to extract the actual location from an item, depending on available attributes: * "fslocation": a pair (path, lineno) @@ -342,9 +344,10 @@ def get_fslocation_from_item(item): :rtype: a tuple of (str|LocalPath, int) with filename and line number. """ - result = getattr(item, "location", None) - if result is not None: - return result[:2] + try: + return item.location[:2] + except AttributeError: + pass obj = getattr(item, "obj", None) if obj is not None: return getfslineno(obj) diff --git a/src/_pytest/python.py b/src/_pytest/python.py index 525498de2..5309c8dd0 100644 --- a/src/_pytest/python.py +++ b/src/_pytest/python.py @@ -10,6 +10,7 @@ from collections import defaultdict from collections.abc import Sequence from functools import partial from textwrap import dedent +from typing import Dict from typing import List from typing import Tuple from typing import Union @@ -36,6 +37,7 @@ from _pytest.compat import STRING_TYPES from _pytest.config import hookimpl from _pytest.deprecated import FUNCARGNAMES from _pytest.mark import MARK_GEN +from _pytest.mark import ParameterSet from _pytest.mark.structures import get_unpacked_marks from _pytest.mark.structures import normalize_mark_list from _pytest.outcomes import fail @@ -947,7 +949,6 @@ class Metafunc: to set a dynamic scope using test context or configuration. """ from _pytest.fixtures import scope2index - from _pytest.mark import ParameterSet argnames, parameters = ParameterSet._for_parametrize( argnames, @@ -996,7 +997,9 @@ class Metafunc: newcalls.append(newcallspec) self._calls = newcalls - def _resolve_arg_ids(self, argnames, ids, parameters, item): + def _resolve_arg_ids( + self, argnames: List[str], ids, parameters: List[ParameterSet], item: nodes.Item + ): """Resolves the actual ids for the given argnames, based on the ``ids`` parameter given to ``parametrize``. @@ -1028,7 +1031,7 @@ class Metafunc: ids = idmaker(argnames, parameters, idfn, ids, self.config, item=item) return ids - def _resolve_arg_value_types(self, argnames, indirect): + def _resolve_arg_value_types(self, argnames: List[str], indirect) -> Dict[str, str]: """Resolves if each parametrized argument must be considered a parameter to a fixture or a "funcarg" to the function, based on the ``indirect`` parameter of the parametrized() call. From a62d9a40e7aaf1936f99436fdbd3b7bc7d5994c1 Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Mon, 10 Feb 2020 10:28:41 +0100 Subject: [PATCH 18/20] ci: Travis: 3.5.1: upgrade pip, setuptools, virtualenv Ref: https://github.com/jaraco/zipp/issues/40 --- .travis.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.travis.yml b/.travis.yml index 59c7951e4..d773d4ab5 100644 --- a/.travis.yml +++ b/.travis.yml @@ -46,6 +46,9 @@ jobs: - env: TOXENV=py35 PYTEST_COVERAGE=1 PYTEST_ADDOPTS="-k test_raises_cyclic_reference" python: '3.5.1' dist: trusty + before_install: + # Work around https://github.com/jaraco/zipp/issues/40. + - python -m pip install -U pip 'setuptools>=34.4.0' virtualenv before_script: - | From 12824e62798165cd39306420ffa440e3987e87b1 Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Mon, 10 Feb 2020 10:59:28 +0100 Subject: [PATCH 19/20] ci: Travis: remove non-py35 jobs --- .travis.yml | 32 ++------------------------------ 1 file changed, 2 insertions(+), 30 deletions(-) diff --git a/.travis.yml b/.travis.yml index d773d4ab5..32ab7f6fd 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,6 +1,6 @@ language: python -dist: xenial -python: '3.7' +dist: trusty +python: '3.5.1' cache: false env: @@ -16,36 +16,8 @@ install: jobs: include: - # OSX tests - first (in test stage), since they are the slower ones. - # Coverage for: - # - osx - # - verbose=1 - - os: osx - osx_image: xcode10.1 - language: generic - env: TOXENV=py37-xdist PYTEST_COVERAGE=1 PYTEST_ADDOPTS=-v - before_install: - - which python3 - - python3 -V - - ln -sfn "$(which python3)" /usr/local/bin/python - - python -V - - test $(python -c 'import sys; print("%d%d" % sys.version_info[0:2])') = 37 - - # Full run of latest supported version, without xdist. - # Coverage for: - # - pytester's LsofFdLeakChecker - # - TestArgComplete (linux only) - # - numpy - # - old attrs - # - verbose=0 - # - test_sys_breakpoint_interception (via pexpect). - - env: TOXENV=py37-lsof-numpy-oldattrs-pexpect-twisted PYTEST_COVERAGE=1 PYTEST_ADDOPTS= - python: '3.7' - # Coverage for Python 3.5.{0,1} specific code, mostly typing related. - env: TOXENV=py35 PYTEST_COVERAGE=1 PYTEST_ADDOPTS="-k test_raises_cyclic_reference" - python: '3.5.1' - dist: trusty before_install: # Work around https://github.com/jaraco/zipp/issues/40. - python -m pip install -U pip 'setuptools>=34.4.0' virtualenv From 449290406c37010f04bcc114b3af356bb1ae50f8 Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Mon, 10 Feb 2020 11:52:19 +0100 Subject: [PATCH 20/20] test_argcomplete: remove usage of `distutils.spawn` (#6703) Fixes collection error with Python 3.5.3 (Travis): testing/test_parseopt.py:2: in import distutils.spawn .tox/py35-coverage/lib/python3.5/distutils/__init__.py:4: in import imp .tox/py35-coverage/lib/python3.5/imp.py:33: in PendingDeprecationWarning, stacklevel=2) E PendingDeprecationWarning: the imp module is deprecated in favour of importlib; see the module's documentation for alternative uses Build log: https://travis-ci.org/blueyed/pytest/builds/648305304 --- testing/test_parseopt.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/testing/test_parseopt.py b/testing/test_parseopt.py index ded5167d8..7c94fdb1e 100644 --- a/testing/test_parseopt.py +++ b/testing/test_parseopt.py @@ -1,7 +1,7 @@ import argparse -import distutils.spawn import os import shlex +import shutil import sys import py @@ -291,7 +291,7 @@ class TestParser: def test_argcomplete(testdir, monkeypatch): - if not distutils.spawn.find_executable("bash"): + if not shutil.which("bash"): pytest.skip("bash not available") script = str(testdir.tmpdir.join("test_argcomplete"))