From 1cbb0c3554e593abfe4633fdbe0a3d554f335d0c Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Fri, 6 Nov 2020 19:25:40 +0200 Subject: [PATCH] Stop importing `pytest` to avoid upcoming import cycles Don't import `pytest` from within some `_pytest` modules since an upcoming commit will import from them into `pytest`. It would have been nice not to have to do it, so that internal plugins look more like external plugins, but with the existing layout this seems unavoidable. --- src/_pytest/cacheprovider.py | 15 +++++++------ src/_pytest/capture.py | 28 +++++++++++++----------- src/_pytest/logging.py | 32 ++++++++++++++------------- src/_pytest/monkeypatch.py | 4 ++-- src/_pytest/pytester.py | 42 ++++++++++++++++++++---------------- src/_pytest/terminal.py | 15 +++++++------ src/_pytest/tmpdir.py | 10 ++++----- 7 files changed, 79 insertions(+), 67 deletions(-) diff --git a/src/_pytest/cacheprovider.py b/src/_pytest/cacheprovider.py index 09f3d6653..1689b9a41 100755 --- a/src/_pytest/cacheprovider.py +++ b/src/_pytest/cacheprovider.py @@ -15,7 +15,6 @@ from typing import Union import attr import py -import pytest from .pathlib import resolve_from_str from .pathlib import rm_rf from .reports import CollectReport @@ -24,7 +23,9 @@ from _pytest._io import TerminalWriter from _pytest.compat import final from _pytest.config import Config from _pytest.config import ExitCode +from _pytest.config import hookimpl from _pytest.config.argparsing import Parser +from _pytest.fixtures import fixture from _pytest.fixtures import FixtureRequest from _pytest.main import Session from _pytest.python import Module @@ -182,7 +183,7 @@ class LFPluginCollWrapper: self.lfplugin = lfplugin self._collected_at_least_one_failure = False - @pytest.hookimpl(hookwrapper=True) + @hookimpl(hookwrapper=True) def pytest_make_collect_report(self, collector: nodes.Collector): if isinstance(collector, Session): out = yield @@ -229,7 +230,7 @@ class LFPluginCollSkipfiles: def __init__(self, lfplugin: "LFPlugin") -> None: self.lfplugin = lfplugin - @pytest.hookimpl + @hookimpl def pytest_make_collect_report( self, collector: nodes.Collector ) -> Optional[CollectReport]: @@ -291,7 +292,7 @@ class LFPlugin: else: self.lastfailed[report.nodeid] = True - @pytest.hookimpl(hookwrapper=True, tryfirst=True) + @hookimpl(hookwrapper=True, tryfirst=True) def pytest_collection_modifyitems( self, config: Config, items: List[nodes.Item] ) -> Generator[None, None, None]: @@ -363,7 +364,7 @@ class NFPlugin: assert config.cache is not None self.cached_nodeids = set(config.cache.get("cache/nodeids", [])) - @pytest.hookimpl(hookwrapper=True, tryfirst=True) + @hookimpl(hookwrapper=True, tryfirst=True) def pytest_collection_modifyitems( self, items: List[nodes.Item] ) -> Generator[None, None, None]: @@ -466,14 +467,14 @@ def pytest_cmdline_main(config: Config) -> Optional[Union[int, ExitCode]]: return None -@pytest.hookimpl(tryfirst=True) +@hookimpl(tryfirst=True) def pytest_configure(config: Config) -> None: config.cache = Cache.for_config(config) config.pluginmanager.register(LFPlugin(config), "lfplugin") config.pluginmanager.register(NFPlugin(config), "nfplugin") -@pytest.fixture +@fixture def cache(request: FixtureRequest) -> Cache: """Return a cache object that can persist state between testing sessions. diff --git a/src/_pytest/capture.py b/src/_pytest/capture.py index 25535f67b..1c3a2b819 100644 --- a/src/_pytest/capture.py +++ b/src/_pytest/capture.py @@ -17,12 +17,14 @@ from typing import Tuple from typing import TYPE_CHECKING from typing import Union -import pytest from _pytest.compat import final from _pytest.config import Config +from _pytest.config import hookimpl from _pytest.config.argparsing import Parser +from _pytest.fixtures import fixture from _pytest.fixtures import SubRequest from _pytest.nodes import Collector +from _pytest.nodes import File from _pytest.nodes import Item if TYPE_CHECKING: @@ -145,7 +147,7 @@ def _py36_windowsconsoleio_workaround(stream: TextIO) -> None: sys.stderr = _reopen_stdio(sys.stderr, "wb") -@pytest.hookimpl(hookwrapper=True) +@hookimpl(hookwrapper=True) def pytest_load_initial_conftests(early_config: Config): ns = early_config.known_args_namespace if ns.capture == "fd": @@ -784,9 +786,9 @@ class CaptureManager: # Hooks - @pytest.hookimpl(hookwrapper=True) + @hookimpl(hookwrapper=True) def pytest_make_collect_report(self, collector: Collector): - if isinstance(collector, pytest.File): + if isinstance(collector, File): self.resume_global_capture() outcome = yield self.suspend_global_capture() @@ -799,26 +801,26 @@ class CaptureManager: else: yield - @pytest.hookimpl(hookwrapper=True) + @hookimpl(hookwrapper=True) def pytest_runtest_setup(self, item: Item) -> Generator[None, None, None]: with self.item_capture("setup", item): yield - @pytest.hookimpl(hookwrapper=True) + @hookimpl(hookwrapper=True) def pytest_runtest_call(self, item: Item) -> Generator[None, None, None]: with self.item_capture("call", item): yield - @pytest.hookimpl(hookwrapper=True) + @hookimpl(hookwrapper=True) def pytest_runtest_teardown(self, item: Item) -> Generator[None, None, None]: with self.item_capture("teardown", item): yield - @pytest.hookimpl(tryfirst=True) + @hookimpl(tryfirst=True) def pytest_keyboard_interrupt(self) -> None: self.stop_global_capturing() - @pytest.hookimpl(tryfirst=True) + @hookimpl(tryfirst=True) def pytest_internalerror(self) -> None: self.stop_global_capturing() @@ -893,7 +895,7 @@ class CaptureFixture(Generic[AnyStr]): # The fixtures. -@pytest.fixture +@fixture def capsys(request: SubRequest) -> Generator[CaptureFixture[str], None, None]: """Enable text capturing of writes to ``sys.stdout`` and ``sys.stderr``. @@ -910,7 +912,7 @@ def capsys(request: SubRequest) -> Generator[CaptureFixture[str], None, None]: capman.unset_fixture() -@pytest.fixture +@fixture def capsysbinary(request: SubRequest) -> Generator[CaptureFixture[bytes], None, None]: """Enable bytes capturing of writes to ``sys.stdout`` and ``sys.stderr``. @@ -927,7 +929,7 @@ def capsysbinary(request: SubRequest) -> Generator[CaptureFixture[bytes], None, capman.unset_fixture() -@pytest.fixture +@fixture def capfd(request: SubRequest) -> Generator[CaptureFixture[str], None, None]: """Enable text capturing of writes to file descriptors ``1`` and ``2``. @@ -944,7 +946,7 @@ def capfd(request: SubRequest) -> Generator[CaptureFixture[str], None, None]: capman.unset_fixture() -@pytest.fixture +@fixture def capfdbinary(request: SubRequest) -> Generator[CaptureFixture[bytes], None, None]: """Enable bytes capturing of writes to file descriptors ``1`` and ``2``. diff --git a/src/_pytest/logging.py b/src/_pytest/logging.py index 3b046c954..2f5da8e7a 100644 --- a/src/_pytest/logging.py +++ b/src/_pytest/logging.py @@ -16,7 +16,6 @@ from typing import Tuple from typing import TypeVar from typing import Union -import pytest from _pytest import nodes from _pytest._io import TerminalWriter from _pytest.capture import CaptureManager @@ -25,7 +24,10 @@ from _pytest.compat import nullcontext from _pytest.config import _strtobool from _pytest.config import Config from _pytest.config import create_terminal_writer +from _pytest.config import hookimpl +from _pytest.config import UsageError from _pytest.config.argparsing import Parser +from _pytest.fixtures import fixture from _pytest.fixtures import FixtureRequest from _pytest.main import Session from _pytest.store import StoreKey @@ -468,7 +470,7 @@ class LogCaptureFixture: self.handler.setLevel(handler_orig_level) -@pytest.fixture +@fixture def caplog(request: FixtureRequest) -> Generator[LogCaptureFixture, None, None]: """Access and control log capturing. @@ -501,7 +503,7 @@ def get_log_level_for_setting(config: Config, *setting_names: str) -> Optional[i return int(getattr(logging, log_level, log_level)) except ValueError as e: # Python logging does not recognise this as a logging level - raise pytest.UsageError( + raise UsageError( "'{}' is not recognized as a logging level name for " "'{}'. Please consider passing the " "logging level num instead.".format(log_level, setting_name) @@ -509,7 +511,7 @@ def get_log_level_for_setting(config: Config, *setting_names: str) -> Optional[i # run after terminalreporter/capturemanager are configured -@pytest.hookimpl(trylast=True) +@hookimpl(trylast=True) def pytest_configure(config: Config) -> None: config.pluginmanager.register(LoggingPlugin(config), "logging-plugin") @@ -639,7 +641,7 @@ class LoggingPlugin: return True - @pytest.hookimpl(hookwrapper=True, tryfirst=True) + @hookimpl(hookwrapper=True, tryfirst=True) def pytest_sessionstart(self) -> Generator[None, None, None]: self.log_cli_handler.set_when("sessionstart") @@ -647,7 +649,7 @@ class LoggingPlugin: with catching_logs(self.log_file_handler, level=self.log_file_level): yield - @pytest.hookimpl(hookwrapper=True, tryfirst=True) + @hookimpl(hookwrapper=True, tryfirst=True) def pytest_collection(self) -> Generator[None, None, None]: self.log_cli_handler.set_when("collection") @@ -655,7 +657,7 @@ class LoggingPlugin: with catching_logs(self.log_file_handler, level=self.log_file_level): yield - @pytest.hookimpl(hookwrapper=True) + @hookimpl(hookwrapper=True) def pytest_runtestloop(self, session: Session) -> Generator[None, None, None]: if session.config.option.collectonly: yield @@ -669,12 +671,12 @@ class LoggingPlugin: with catching_logs(self.log_file_handler, level=self.log_file_level): yield # Run all the tests. - @pytest.hookimpl + @hookimpl def pytest_runtest_logstart(self) -> None: self.log_cli_handler.reset() self.log_cli_handler.set_when("start") - @pytest.hookimpl + @hookimpl def pytest_runtest_logreport(self) -> None: self.log_cli_handler.set_when("logreport") @@ -695,7 +697,7 @@ class LoggingPlugin: log = report_handler.stream.getvalue().strip() item.add_report_section(when, "log", log) - @pytest.hookimpl(hookwrapper=True) + @hookimpl(hookwrapper=True) def pytest_runtest_setup(self, item: nodes.Item) -> Generator[None, None, None]: self.log_cli_handler.set_when("setup") @@ -703,13 +705,13 @@ class LoggingPlugin: item._store[caplog_records_key] = empty yield from self._runtest_for(item, "setup") - @pytest.hookimpl(hookwrapper=True) + @hookimpl(hookwrapper=True) def pytest_runtest_call(self, item: nodes.Item) -> Generator[None, None, None]: self.log_cli_handler.set_when("call") yield from self._runtest_for(item, "call") - @pytest.hookimpl(hookwrapper=True) + @hookimpl(hookwrapper=True) def pytest_runtest_teardown(self, item: nodes.Item) -> Generator[None, None, None]: self.log_cli_handler.set_when("teardown") @@ -717,11 +719,11 @@ class LoggingPlugin: del item._store[caplog_records_key] del item._store[caplog_handler_key] - @pytest.hookimpl + @hookimpl def pytest_runtest_logfinish(self) -> None: self.log_cli_handler.set_when("finish") - @pytest.hookimpl(hookwrapper=True, tryfirst=True) + @hookimpl(hookwrapper=True, tryfirst=True) def pytest_sessionfinish(self) -> Generator[None, None, None]: self.log_cli_handler.set_when("sessionfinish") @@ -729,7 +731,7 @@ class LoggingPlugin: with catching_logs(self.log_file_handler, level=self.log_file_level): yield - @pytest.hookimpl + @hookimpl def pytest_unconfigure(self) -> None: # Close the FileHandler explicitly. # (logging.shutdown might have lost the weakref?!) diff --git a/src/_pytest/monkeypatch.py b/src/_pytest/monkeypatch.py index df4726705..a50c7c8d5 100644 --- a/src/_pytest/monkeypatch.py +++ b/src/_pytest/monkeypatch.py @@ -15,9 +15,9 @@ from typing import Tuple from typing import TypeVar from typing import Union -import pytest from _pytest.compat import final from _pytest.fixtures import fixture +from _pytest.warning_types import PytestWarning RE_IMPORT_ERROR_NAME = re.compile(r"^No module named (.*)$") @@ -271,7 +271,7 @@ class MonkeyPatch: """ if not isinstance(value, str): warnings.warn( # type: ignore[unreachable] - pytest.PytestWarning( + PytestWarning( "Value of environment variable {name} type should be str, but got " "{value!r} (type: {type}); converted to str implicitly".format( name=name, value=value, type=type(value).__name__ diff --git a/src/_pytest/pytester.py b/src/_pytest/pytester.py index 43ccc97c6..0e1dffb9d 100644 --- a/src/_pytest/pytester.py +++ b/src/_pytest/pytester.py @@ -34,7 +34,6 @@ import py from iniconfig import IniConfig from iniconfig import SectionWrapper -import pytest from _pytest import timing from _pytest._code import Source from _pytest.capture import _get_multicapture @@ -42,17 +41,24 @@ from _pytest.compat import final from _pytest.config import _PluggyPlugin from _pytest.config import Config from _pytest.config import ExitCode +from _pytest.config import hookimpl +from _pytest.config import main from _pytest.config import PytestPluginManager from _pytest.config.argparsing import Parser +from _pytest.fixtures import fixture from _pytest.fixtures import FixtureRequest from _pytest.main import Session from _pytest.monkeypatch import MonkeyPatch from _pytest.nodes import Collector from _pytest.nodes import Item +from _pytest.outcomes import fail +from _pytest.outcomes import importorskip +from _pytest.outcomes import skip from _pytest.pathlib import make_numbered_dir from _pytest.reports import CollectReport from _pytest.reports import TestReport from _pytest.tmpdir import TempPathFactory +from _pytest.warning_types import PytestWarning if TYPE_CHECKING: from typing_extensions import Literal @@ -143,7 +149,7 @@ class LsofFdLeakChecker: else: return True - @pytest.hookimpl(hookwrapper=True, tryfirst=True) + @hookimpl(hookwrapper=True, tryfirst=True) def pytest_runtest_protocol(self, item: Item) -> Generator[None, None, None]: lines1 = self.get_open_files() yield @@ -165,13 +171,13 @@ class LsofFdLeakChecker: "*** function %s:%s: %s " % item.location, "See issue #2366", ] - item.warn(pytest.PytestWarning("\n".join(error))) + item.warn(PytestWarning("\n".join(error))) # used at least by pytest-xdist plugin -@pytest.fixture +@fixture def _pytest(request: FixtureRequest) -> "PytestArg": """Return a helper which offers a gethookrecorder(hook) method which returns a HookRecorder instance which helps to make assertions about called @@ -257,7 +263,7 @@ class HookRecorder: break print("NONAMEMATCH", name, "with", call) else: - pytest.fail(f"could not find {name!r} check {check!r}") + fail(f"could not find {name!r} check {check!r}") def popcall(self, name: str) -> ParsedCall: __tracebackhide__ = True @@ -267,7 +273,7 @@ class HookRecorder: return call lines = [f"could not find call {name!r}, in:"] lines.extend([" %s" % x for x in self.calls]) - pytest.fail("\n".join(lines)) + fail("\n".join(lines)) def getcall(self, name: str) -> ParsedCall: values = self.getcalls(name) @@ -417,14 +423,14 @@ class HookRecorder: self.calls[:] = [] -@pytest.fixture +@fixture def linecomp() -> "LineComp": """A :class: `LineComp` instance for checking that an input linearly contains a sequence of strings.""" return LineComp() -@pytest.fixture(name="LineMatcher") +@fixture(name="LineMatcher") def LineMatcher_fixture(request: FixtureRequest) -> Type["LineMatcher"]: """A reference to the :class: `LineMatcher`. @@ -434,7 +440,7 @@ def LineMatcher_fixture(request: FixtureRequest) -> Type["LineMatcher"]: return LineMatcher -@pytest.fixture +@fixture def pytester(request: FixtureRequest, tmp_path_factory: TempPathFactory) -> "Pytester": """ Facilities to write tests/configuration files, execute pytest in isolation, and match @@ -449,7 +455,7 @@ def pytester(request: FixtureRequest, tmp_path_factory: TempPathFactory) -> "Pyt return Pytester(request, tmp_path_factory) -@pytest.fixture +@fixture def testdir(pytester: "Pytester") -> "Testdir": """ Identical to :fixture:`pytester`, and provides an instance whose methods return @@ -460,7 +466,7 @@ def testdir(pytester: "Pytester") -> "Testdir": return Testdir(pytester) -@pytest.fixture +@fixture def _sys_snapshot() -> Generator[None, None, None]: snappaths = SysPathsSnapshot() snapmods = SysModulesSnapshot() @@ -469,7 +475,7 @@ def _sys_snapshot() -> Generator[None, None, None]: snappaths.restore() -@pytest.fixture +@fixture def _config_for_test() -> Generator[Config, None, None]: from _pytest.config import get_config @@ -495,7 +501,7 @@ class RunResult: duration: float, ) -> None: try: - self.ret: Union[int, ExitCode] = pytest.ExitCode(ret) + self.ret: Union[int, ExitCode] = ExitCode(ret) """The return value.""" except ValueError: self.ret = ret @@ -1062,7 +1068,7 @@ class Pytester: rec.append(self.make_hook_recorder(config.pluginmanager)) plugins.append(Collect()) - ret = pytest.main([str(x) for x in args], plugins=plugins) + ret = main([str(x) for x in args], plugins=plugins) if len(rec) == 1: reprec = rec.pop() else: @@ -1448,11 +1454,11 @@ class Pytester: The pexpect child is returned. """ - pexpect = pytest.importorskip("pexpect", "3.0") + pexpect = importorskip("pexpect", "3.0") if hasattr(sys, "pypy_version_info") and "64" in platform.machine(): - pytest.skip("pypy-64 bit not supported") + skip("pypy-64 bit not supported") if not hasattr(pexpect, "spawn"): - pytest.skip("pexpect.spawn not available") + skip("pexpect.spawn not available") logfile = self.path.joinpath("spawn.out").open("wb") child = pexpect.spawn(cmd, logfile=logfile, timeout=expect_timeout) @@ -1899,7 +1905,7 @@ class LineMatcher: __tracebackhide__ = True log_text = self._log_text self._log_output = [] - pytest.fail(log_text) + fail(log_text) def str(self) -> str: """Return the entire original text.""" diff --git a/src/_pytest/terminal.py b/src/_pytest/terminal.py index ff28be565..8ea67f3b5 100644 --- a/src/_pytest/terminal.py +++ b/src/_pytest/terminal.py @@ -29,7 +29,7 @@ import attr import pluggy import py -import pytest +import _pytest._version from _pytest import nodes from _pytest import timing from _pytest._code import ExceptionInfo @@ -39,6 +39,7 @@ from _pytest.compat import final from _pytest.config import _PluggyPlugin from _pytest.config import Config from _pytest.config import ExitCode +from _pytest.config import hookimpl from _pytest.config.argparsing import Parser from _pytest.nodes import Item from _pytest.nodes import Node @@ -259,7 +260,7 @@ def getreportopt(config: Config) -> str: return reportopts -@pytest.hookimpl(trylast=True) # after _pytest.runner +@hookimpl(trylast=True) # after _pytest.runner def pytest_report_teststatus(report: BaseReport) -> Tuple[str, str, str]: letter = "F" if report.passed: @@ -628,7 +629,7 @@ class TerminalReporter: self._add_stats("error", [report]) elif report.skipped: self._add_stats("skipped", [report]) - items = [x for x in report.result if isinstance(x, pytest.Item)] + items = [x for x in report.result if isinstance(x, Item)] self._numcollected += len(items) if self.isatty: self.report_collect() @@ -673,7 +674,7 @@ class TerminalReporter: else: self.write_line(line) - @pytest.hookimpl(trylast=True) + @hookimpl(trylast=True) def pytest_sessionstart(self, session: "Session") -> None: self._session = session self._sessionstarttime = timing.time() @@ -688,7 +689,7 @@ class TerminalReporter: verinfo = ".".join(map(str, pypy_version_info[:3])) msg += "[pypy-{}-{}]".format(verinfo, pypy_version_info[3]) msg += ", pytest-{}, py-{}, pluggy-{}".format( - pytest.__version__, py.__version__, pluggy.__version__ + _pytest._version.version, py.__version__, pluggy.__version__ ) if ( self.verbosity > 0 @@ -783,7 +784,7 @@ class TerminalReporter: for line in doc.splitlines(): self._tw.line("{}{}".format(indent + " ", line)) - @pytest.hookimpl(hookwrapper=True) + @hookimpl(hookwrapper=True) def pytest_sessionfinish( self, session: "Session", exitstatus: Union[int, ExitCode] ): @@ -810,7 +811,7 @@ class TerminalReporter: self.write_sep("!", str(session.shouldstop), red=True) self.summary_stats() - @pytest.hookimpl(hookwrapper=True) + @hookimpl(hookwrapper=True) def pytest_terminal_summary(self) -> Generator[None, None, None]: self.summary_errors() self.summary_failures() diff --git a/src/_pytest/tmpdir.py b/src/_pytest/tmpdir.py index b889be888..4ca1dd6e1 100644 --- a/src/_pytest/tmpdir.py +++ b/src/_pytest/tmpdir.py @@ -8,13 +8,13 @@ from typing import Optional import attr import py -import pytest from .pathlib import ensure_reset_dir from .pathlib import LOCK_TIMEOUT from .pathlib import make_numbered_dir from .pathlib import make_numbered_dir_with_cleanup from _pytest.compat import final from _pytest.config import Config +from _pytest.fixtures import fixture from _pytest.fixtures import FixtureRequest from _pytest.monkeypatch import MonkeyPatch @@ -146,14 +146,14 @@ def pytest_configure(config: Config) -> None: mp.setattr(config, "_tmpdirhandler", t, raising=False) -@pytest.fixture(scope="session") +@fixture(scope="session") def tmpdir_factory(request: FixtureRequest) -> TempdirFactory: """Return a :class:`_pytest.tmpdir.TempdirFactory` instance for the test session.""" # Set dynamically by pytest_configure() above. return request.config._tmpdirhandler # type: ignore -@pytest.fixture(scope="session") +@fixture(scope="session") def tmp_path_factory(request: FixtureRequest) -> TempPathFactory: """Return a :class:`_pytest.tmpdir.TempPathFactory` instance for the test session.""" # Set dynamically by pytest_configure() above. @@ -168,7 +168,7 @@ def _mk_tmp(request: FixtureRequest, factory: TempPathFactory) -> Path: return factory.mktemp(name, numbered=True) -@pytest.fixture +@fixture def tmpdir(tmp_path: Path) -> py.path.local: """Return a temporary directory path object which is unique to each test function invocation, created as a sub directory of the base temporary @@ -181,7 +181,7 @@ def tmpdir(tmp_path: Path) -> py.path.local: return py.path.local(tmp_path) -@pytest.fixture +@fixture def tmp_path(request: FixtureRequest, tmp_path_factory: TempPathFactory) -> Path: """Return a temporary directory path object which is unique to each test function invocation, created as a sub directory of the base temporary