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/18] 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 bd2fd9871f7..85521309fb6 100644 --- a/doc/en/conf.py +++ b/doc/en/conf.py @@ -162,7 +162,7 @@ # 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/18] 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 1b94aaf00d7..65ef1272b78 100644 --- a/src/_pytest/python.py +++ b/src/_pytest/python.py @@ -390,7 +390,7 @@ def _genfunctions(self, name, funcobj): 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/18] 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 e5666da9fce..8ef06db3811 100644 --- a/src/_pytest/main.py +++ b/src/_pytest/main.py @@ -5,6 +5,7 @@ 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 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/18] 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 00000000000..bcc2e1d9467 --- /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 8ef06db3811..59c3c6714f8 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 b47791b29c1..49e3decd0c9 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 pytest_internalerror(excrepr, excinfo): 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/18] 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 50e32d660a2..088f6a0651f 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/18] 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 3d7a073232c..00000000000 --- 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/18] 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 296aa42aaac..ffc8fd38aba 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/18] 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 7f9aa9556de..2e221f73ec6 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/18] 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 379393b10cd..b5e18863ffc 100644 --- a/src/_pytest/_code/source.py +++ b/src/_pytest/_code/source.py @@ -17,6 +17,7 @@ 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 085f634a4eb..d6ee1d522fc 100644 --- a/src/_pytest/compat.py +++ b/src/_pytest/compat.py @@ -22,7 +22,6 @@ 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 5b3686b5807..a6bfeb6d303 100644 --- a/src/_pytest/fixtures.py +++ b/src/_pytest/fixtures.py @@ -16,12 +16,12 @@ 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 3002f8abc41..de4333a624b 100644 --- a/src/_pytest/mark/structures.py +++ b/src/_pytest/mark/structures.py @@ -6,9 +6,9 @@ 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 5447f254173..218684e1481 100644 --- a/src/_pytest/nodes.py +++ b/src/_pytest/nodes.py @@ -15,8 +15,8 @@ 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 65ef1272b78..525498de22a 100644 --- a/src/_pytest/python.py +++ b/src/_pytest/python.py @@ -20,10 +20,10 @@ 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/18] 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 b176dde98b9..cafd870f04b 100644 --- a/src/_pytest/_code/code.py +++ b/src/_pytest/_code/code.py @@ -72,6 +72,8 @@ def path(self) -> Union[py.path.local, str]: """ 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 b5efdb31702..cf09309744a 100644 --- a/testing/code/test_source.py +++ b/testing/code/test_source.py @@ -524,6 +524,14 @@ class B: 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/18] 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 b5e18863ffc..432e1cbe823 100644 --- a/src/_pytest/_code/source.py +++ b/src/_pytest/_code/source.py @@ -8,6 +8,7 @@ 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 d6ee1d522fc..3a3645c5a3f 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/18] 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 3a3645c5a3f..f204dbd2dfe 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/18] 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 00000000000..4dba3ed0723 --- /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 a060723a76b..cdb0347034f 100644 --- a/src/_pytest/assertion/__init__.py +++ b/src/_pytest/assertion/__init__.py @@ -8,6 +8,7 @@ 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 callbinrepr(op, left, right): 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 call_assertion_pass_hook(lineno, orig, expl): 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 ed3334e5fc4..d4477ba81d8 100644 --- a/src/_pytest/config/__init__.py +++ b/src/_pytest/config/__init__.py @@ -27,7 +27,6 @@ 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 @@ def _consider_importhook(self, args): 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 e975a3fea2b..dc260b39f9e 100644 --- a/testing/test_assertion.py +++ b/testing/test_assertion.py @@ -72,10 +72,19 @@ def test_dummy_failure(testdir): # how meta! 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/18] 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 f65a60b44c4..9bc7367c830 100644 --- a/testing/acceptance_test.py +++ b/testing/acceptance_test.py @@ -104,6 +104,8 @@ def test_option(pytestconfig): @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 33b817a1226..3127fda6a83 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 runtest(self): """ ) 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 1dee5b0f51d..a06ba0e2667 100644 --- a/testing/test_helpconfig.py +++ b/testing/test_helpconfig.py @@ -3,6 +3,7 @@ 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 365332d7016..6532a89b152 100644 --- a/testing/test_junitxml.py +++ b/testing/test_junitxml.py @@ -1227,6 +1227,7 @@ def test_pass(): 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 c3a0c17e1d5..cc2f6d5fb69 100644 --- a/testing/test_terminal.py +++ b/testing/test_terminal.py @@ -604,6 +604,7 @@ def test_method(self): 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 @@ def test_verbose_reporting_xdist(self, verbose_testfile, testdir, pytestconfig): 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/18] 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 cfe1b9a6ca5..60088502ee7 100644 --- a/src/_pytest/pytester.py +++ b/src/_pytest/pytester.py @@ -608,14 +608,14 @@ def chdir(self): """ 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/18] 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 206e44d9618..c99c79f1035 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 c0f0849e92e8603136ce879a0d6783d6db950fdc Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Sat, 8 Feb 2020 05:50:07 +0100 Subject: [PATCH 17/18] ci: Travis: upgrade pip --- .travis.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.travis.yml b/.travis.yml index 59c7951e407..f3f817cf994 100644 --- a/.travis.yml +++ b/.travis.yml @@ -12,6 +12,7 @@ git: depth: false install: + - python -m pip install --upgrade pip - python -m pip install --upgrade --pre tox jobs: From 32a09202bd5e61d9a896ee3d83acb1db63613911 Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Sat, 8 Feb 2020 05:52:01 +0100 Subject: [PATCH 18/18] ci: Travis: tox -vv --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index f3f817cf994..76d7ef65b5a 100644 --- a/.travis.yml +++ b/.travis.yml @@ -62,7 +62,7 @@ before_script: export _PYTEST_TOX_EXTRA_DEP=coverage-enable-subprocess fi -script: tox +script: tox -vv after_success: - |