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 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/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 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/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 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/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 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/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 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/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 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/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 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/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 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/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 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/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 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/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 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/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 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/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 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/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 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/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 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/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 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 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 df0da3daae5..3fccee00572 100644 --- a/src/_pytest/logging.py +++ b/src/_pytest/logging.py @@ -10,6 +10,7 @@ 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 @@ def _finalize(self) -> None: 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 @@ def get_records(self, when): """ 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 @@ def _runtest_for(self, item, when): 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 @@ def _runtest_for_main(self, item, when): 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 de4333a624b..161f623ee69 100644 --- a/src/_pytest/mark/structures.py +++ b/src/_pytest/mark/structures.py @@ -2,7 +2,10 @@ 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 218684e1481..641f889fe3f 100644 --- a/src/_pytest/nodes.py +++ b/src/_pytest/nodes.py @@ -333,7 +333,9 @@ def repr_failure( 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 525498de22a..5309c8dd038 100644 --- a/src/_pytest/python.py +++ b/src/_pytest/python.py @@ -10,6 +10,7 @@ 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.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 @@ def parametrize(self, argnames, argvalues, indirect=False, ids=None, scope=None) 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 @@ def parametrize(self, argnames, argvalues, indirect=False, ids=None, scope=None) 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 @@ def _resolve_arg_ids(self, argnames, ids, parameters, item): 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 59c7951e407..d773d4ab50d 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 d773d4ab50d..32ab7f6fd3c 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 ded5167d8da..7c94fdb1e20 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 @@ def test_multiple_metavar_help(self, parser): 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"))