Merge pull request #1060 from bluetech/pre-typing-fixes

Pre-typing fixes/improvements
This commit is contained in:
Ran Benita
2024-04-06 22:26:31 +03:00
committed by GitHub
12 changed files with 70 additions and 84 deletions

View File

@@ -31,4 +31,4 @@ repos:
additional_dependencies: additional_dependencies:
- pytest>=7.0.0 - pytest>=7.0.0
- execnet>=2.1.0 - execnet>=2.1.0
- py>=1.10.0 - types-psutil

View File

@@ -85,6 +85,7 @@ select = [
"W", # pycodestyle "W", # pycodestyle
"T10", # flake8-debugger "T10", # flake8-debugger
"PIE", # flake8-pie "PIE", # flake8-pie
"FA", # flake8-future-annotations
"PGH", # pygrep-hooks "PGH", # pygrep-hooks
"PLE", # pylint error "PLE", # pylint error
"PLW", # pylint warning "PLW", # pylint warning
@@ -135,6 +136,7 @@ lines-after-imports = 2
[tool.mypy] [tool.mypy]
mypy_path = ["src"] mypy_path = ["src"]
files = ["src", "testing"]
# TODO: Enable this & fix errors. # TODO: Enable this & fix errors.
# check_untyped_defs = true # check_untyped_defs = true
disallow_any_generics = true disallow_any_generics = true

View File

@@ -317,13 +317,6 @@ class DSession:
assert not rep.passed assert not rep.passed
self._failed_worker_collectreport(node, rep) self._failed_worker_collectreport(node, rep)
def worker_warning_captured(self, warning_message, when, item):
"""Emitted when a node calls the pytest_warning_captured hook (deprecated in 6.0)."""
# This hook as been removed in pytest 7.1, and we can remove support once we only
# support pytest >=7.1.
kwargs = dict(warning_message=warning_message, when=when, item=item)
self.config.hook.pytest_warning_captured.call_historic(kwargs=kwargs)
def worker_warning_recorded(self, warning_message, when, nodeid, location): def worker_warning_recorded(self, warning_message, when, nodeid, location):
"""Emitted when a node calls the pytest_warning_recorded hook.""" """Emitted when a node calls the pytest_warning_recorded hook."""
kwargs = dict( kwargs = dict(
@@ -374,10 +367,9 @@ class DSession:
def handle_crashitem(self, nodeid, worker): def handle_crashitem(self, nodeid, worker):
# XXX get more reporting info by recording pytest_runtest_logstart? # XXX get more reporting info by recording pytest_runtest_logstart?
# XXX count no of failures and retry N times # XXX count no of failures and retry N times
runner = self.config.pluginmanager.getplugin("runner")
fspath = nodeid.split("::")[0] fspath = nodeid.split("::")[0]
msg = f"worker {worker.gateway.id!r} crashed while running {nodeid!r}" msg = f"worker {worker.gateway.id!r} crashed while running {nodeid!r}"
rep = runner.TestReport( rep = pytest.TestReport(
nodeid, (fspath, None, fspath), (), "failed", msg, "???" nodeid, (fspath, None, fspath), (), "failed", msg, "???"
) )
rep.node = worker rep.node = worker

View File

@@ -7,11 +7,12 @@ processes) otherwise changes to source code can crash
the controlling process which should best never happen. the controlling process which should best never happen.
""" """
from __future__ import annotations
import os import os
from pathlib import Path from pathlib import Path
import sys import sys
import time import time
from typing import Dict
from typing import Sequence from typing import Sequence
from _pytest._io import TerminalWriter from _pytest._io import TerminalWriter
@@ -45,7 +46,7 @@ def pytest_cmdline_main(config):
return 2 # looponfail only can get stop with ctrl-C anyway return 2 # looponfail only can get stop with ctrl-C anyway
def looponfail_main(config: "pytest.Config") -> None: def looponfail_main(config: pytest.Config) -> None:
remotecontrol = RemoteControl(config) remotecontrol = RemoteControl(config)
config_roots = config.getini("looponfailroots") config_roots = config.getini("looponfailroots")
if not config_roots: if not config_roots:
@@ -79,9 +80,7 @@ class RemoteControl:
def initgateway(self): def initgateway(self):
return execnet.makegateway("popen") return execnet.makegateway("popen")
def setup(self, out=None): def setup(self):
if out is None:
out = TerminalWriter()
if hasattr(self, "gateway"): if hasattr(self, "gateway"):
raise ValueError("already have gateway %r" % self.gateway) raise ValueError("already have gateway %r" % self.gateway)
self.trace("setting up worker session") self.trace("setting up worker session")
@@ -93,6 +92,8 @@ class RemoteControl:
) )
remote_outchannel = channel.receive() remote_outchannel = channel.receive()
out = TerminalWriter()
def write(s): def write(s):
out._file.write(s) out._file.write(s)
out._file.flush() out._file.flush()
@@ -238,7 +239,7 @@ class WorkerFailSession:
class StatRecorder: class StatRecorder:
def __init__(self, rootdirlist: Sequence[Path]) -> None: def __init__(self, rootdirlist: Sequence[Path]) -> None:
self.rootdirlist = rootdirlist self.rootdirlist = rootdirlist
self.statcache: Dict[Path, os.stat_result] = {} self.statcache: dict[Path, os.stat_result] = {}
self.check() # snapshot state self.check() # snapshot state
def fil(self, p: Path) -> bool: def fil(self, p: Path) -> bool:
@@ -256,7 +257,7 @@ class StatRecorder:
def check(self, removepycfiles: bool = True) -> bool: def check(self, removepycfiles: bool = True) -> bool:
changed = False changed = False
newstat: Dict[Path, os.stat_result] = {} newstat: dict[Path, os.stat_result] = {}
for rootdir in self.rootdirlist: for rootdir in self.rootdirlist:
for path in visit_path(rootdir, filter=self.fil, recurse=self.rec): for path in visit_path(rootdir, filter=self.fil, recurse=self.rec):
oldstat = self.statcache.pop(path, None) oldstat = self.statcache.pop(path, None)

View File

@@ -7,6 +7,7 @@ needs not to be installed in remote environments.
""" """
import contextlib import contextlib
import enum
import os import os
import sys import sys
import time import time
@@ -57,10 +58,12 @@ def worker_title(title):
pass pass
class WorkerInteractor: class Marker(enum.Enum):
SHUTDOWN_MARK = object() SHUTDOWN = 0
QUEUE_REPLACED_MARK = object() QUEUE_REPLACED = 1
class WorkerInteractor:
def __init__(self, config, channel): def __init__(self, config, channel):
self.config = config self.config = config
self.workerid = config.workerinput.get("workerid", "?") self.workerid = config.workerinput.get("workerid", "?")
@@ -79,7 +82,7 @@ class WorkerInteractor:
is replaced concurrently in another thread. is replaced concurrently in another thread.
""" """
result = self.torun.get() result = self.torun.get()
while result is self.QUEUE_REPLACED_MARK: while result is Marker.QUEUE_REPLACED:
result = self.torun.get() result = self.torun.get()
return result return result
@@ -114,8 +117,8 @@ class WorkerInteractor:
self.sendevent("collectionstart") self.sendevent("collectionstart")
def handle_command(self, command): def handle_command(self, command):
if command is self.SHUTDOWN_MARK: if command is Marker.SHUTDOWN:
self.torun.put(self.SHUTDOWN_MARK) self.torun.put(Marker.SHUTDOWN)
return return
name, kwargs = command name, kwargs = command
@@ -128,7 +131,7 @@ class WorkerInteractor:
for i in range(len(self.session.items)): for i in range(len(self.session.items)):
self.torun.put(i) self.torun.put(i)
elif name == "shutdown": elif name == "shutdown":
self.torun.put(self.SHUTDOWN_MARK) self.torun.put(Marker.SHUTDOWN)
elif name == "steal": elif name == "steal":
self.steal(kwargs["indices"]) self.steal(kwargs["indices"])
@@ -149,14 +152,14 @@ class WorkerInteractor:
self.torun.put(i) self.torun.put(i)
self.sendevent("unscheduled", indices=stolen) self.sendevent("unscheduled", indices=stolen)
old_queue.put(self.QUEUE_REPLACED_MARK) old_queue.put(Marker.QUEUE_REPLACED)
@pytest.hookimpl @pytest.hookimpl
def pytest_runtestloop(self, session): def pytest_runtestloop(self, session):
self.log("entering main loop") self.log("entering main loop")
self.channel.setcallback(self.handle_command, endmarker=self.SHUTDOWN_MARK) self.channel.setcallback(self.handle_command, endmarker=Marker.SHUTDOWN)
self.nextitem_index = self._get_next_item_index() self.nextitem_index = self._get_next_item_index()
while self.nextitem_index is not self.SHUTDOWN_MARK: while self.nextitem_index is not Marker.SHUTDOWN:
self.run_one_test() self.run_one_test()
if session.shouldfail or session.shouldstop: if session.shouldfail or session.shouldstop:
break break
@@ -168,16 +171,16 @@ class WorkerInteractor:
items = self.session.items items = self.session.items
item = items[self.item_index] item = items[self.item_index]
if self.nextitem_index is self.SHUTDOWN_MARK: if self.nextitem_index is Marker.SHUTDOWN:
nextitem = None nextitem = None
else: else:
nextitem = items[self.nextitem_index] nextitem = items[self.nextitem_index]
worker_title("[pytest-xdist running] %s" % item.nodeid) worker_title("[pytest-xdist running] %s" % item.nodeid)
start = time.time() start = time.perf_counter()
self.config.hook.pytest_runtest_protocol(item=item, nextitem=nextitem) self.config.hook.pytest_runtest_protocol(item=item, nextitem=nextitem)
duration = time.time() - start duration = time.perf_counter() - start
worker_title("[pytest-xdist idle]") worker_title("[pytest-xdist idle]")

View File

@@ -101,12 +101,7 @@ class EachScheduling:
self.node2pending[node].remove(item_index) self.node2pending[node].remove(item_index)
def mark_test_pending(self, item): def mark_test_pending(self, item):
self.pending.insert( raise NotImplementedError()
0,
self.collection.index(item),
)
for node in self.node2pending:
self.check_schedule(node)
def remove_node(self, node): def remove_node(self, node):
# KeyError if we didn't get an add_node() yet # KeyError if we didn't get an add_node() yet

View File

@@ -90,8 +90,8 @@ class LoadScopeScheduling:
self.collection = None self.collection = None
self.workqueue = OrderedDict() self.workqueue = OrderedDict()
self.assigned_work = OrderedDict() self.assigned_work = {}
self.registered_collections = OrderedDict() self.registered_collections = {}
if log is None: if log is None:
self.log = Producer("loadscopesched") self.log = Producer("loadscopesched")
@@ -156,7 +156,7 @@ class LoadScopeScheduling:
bootstraps a new node. bootstraps a new node.
""" """
assert node not in self.assigned_work assert node not in self.assigned_work
self.assigned_work[node] = OrderedDict() self.assigned_work[node] = {}
def remove_node(self, node): def remove_node(self, node):
"""Remove a node from the scheduler. """Remove a node from the scheduler.
@@ -252,7 +252,7 @@ class LoadScopeScheduling:
scope, work_unit = self.workqueue.popitem(last=False) scope, work_unit = self.workqueue.popitem(last=False)
# Keep track of the assigned work # Keep track of the assigned work
assigned_to_node = self.assigned_work.setdefault(node, default=OrderedDict()) assigned_to_node = self.assigned_work.setdefault(node, {})
assigned_to_node[scope] = work_unit assigned_to_node[scope] = work_unit
# Ask the node to execute the workload # Ask the node to execute the workload
@@ -349,10 +349,10 @@ class LoadScopeScheduling:
return return
# Determine chunks of work (scopes) # Determine chunks of work (scopes)
unsorted_workqueue = OrderedDict() unsorted_workqueue = {}
for nodeid in self.collection: for nodeid in self.collection:
scope = self._split_scope(nodeid) scope = self._split_scope(nodeid)
work_unit = unsorted_workqueue.setdefault(scope, default=OrderedDict()) work_unit = unsorted_workqueue.setdefault(scope, {})
work_unit[nodeid] = False work_unit[nodeid] = False
# Insert tests scopes into work queue ordered by number of tests. # Insert tests scopes into work queue ordered by number of tests.
@@ -368,7 +368,7 @@ class LoadScopeScheduling:
self.log(f"Shutting down {extra_nodes} nodes") self.log(f"Shutting down {extra_nodes} nodes")
for _ in range(extra_nodes): for _ in range(extra_nodes):
unused_node, assigned = self.assigned_work.popitem(last=True) unused_node, assigned = self.assigned_work.popitem()
self.log(f"Shutting down unused node {unused_node}") self.log(f"Shutting down unused node {unused_node}")
unused_node.shutdown() unused_node.shutdown()
@@ -407,9 +407,6 @@ class LoadScopeScheduling:
same_collection = False same_collection = False
self.log(msg) self.log(msg)
if self.config is None:
continue
rep = pytest.CollectReport( rep = pytest.CollectReport(
nodeid=node.gateway.id, nodeid=node.gateway.id,
outcome="failed", outcome="failed",

View File

@@ -1,14 +1,13 @@
from __future__ import annotations
import enum
import fnmatch import fnmatch
import os import os
from pathlib import Path from pathlib import Path
import re import re
import sys import sys
from typing import Any from typing import Any
from typing import List
from typing import Optional
from typing import Sequence from typing import Sequence
from typing import Set
from typing import Tuple
from typing import Union from typing import Union
import uuid import uuid
@@ -60,7 +59,7 @@ class NodeManager:
self.specs.append(spec) self.specs.append(spec)
self.roots = self._getrsyncdirs() self.roots = self._getrsyncdirs()
self.rsyncoptions = self._getrsyncoptions() self.rsyncoptions = self._getrsyncoptions()
self._rsynced_specs: Set[Tuple[Any, Any]] = set() self._rsynced_specs: set[tuple[Any, Any]] = set()
def rsync_roots(self, gateway): def rsync_roots(self, gateway):
"""Rsync the set of roots to the node's gateway cwd.""" """Rsync the set of roots to the node's gateway cwd."""
@@ -89,7 +88,7 @@ class NodeManager:
def _getxspecs(self): def _getxspecs(self):
return [execnet.XSpec(x) for x in parse_spec_config(self.config)] return [execnet.XSpec(x) for x in parse_spec_config(self.config)]
def _getrsyncdirs(self) -> List[Path]: def _getrsyncdirs(self) -> list[Path]:
for spec in self.specs: for spec in self.specs:
if not spec.popen or spec.chdir: if not spec.popen or spec.chdir:
break break
@@ -174,7 +173,7 @@ class HostRSync(execnet.RSync):
self, self,
sourcedir: PathLike, sourcedir: PathLike,
*, *,
ignores: Optional[Sequence[PathLike]] = None, ignores: Sequence[PathLike] | None = None,
verbose: bool = True, verbose: bool = True,
) -> None: ) -> None:
if ignores is None: if ignores is None:
@@ -201,7 +200,7 @@ class HostRSync(execnet.RSync):
print(f"{gateway.spec}:{remotepath} <= {path}") print(f"{gateway.spec}:{remotepath} <= {path}")
def make_reltoroot(roots: Sequence[Path], args: List[str]) -> List[str]: def make_reltoroot(roots: Sequence[Path], args: list[str]) -> list[str]:
# XXX introduce/use public API for splitting pytest args # XXX introduce/use public API for splitting pytest args
splitcode = "::" splitcode = "::"
result = [] result = []
@@ -216,7 +215,7 @@ def make_reltoroot(roots: Sequence[Path], args: List[str]) -> List[str]:
result.append(arg) result.append(arg)
continue continue
for root in roots: for root in roots:
x: Optional[Path] x: Path | None
try: try:
x = fspath.relative_to(root) x = fspath.relative_to(root)
except ValueError: except ValueError:
@@ -230,9 +229,11 @@ def make_reltoroot(roots: Sequence[Path], args: List[str]) -> List[str]:
return result return result
class WorkerController: class Marker(enum.Enum):
ENDMARK = -1 END = -1
class WorkerController:
class RemoteHook: class RemoteHook:
@pytest.hookimpl(trylast=True) @pytest.hookimpl(trylast=True)
def pytest_xdist_getremotemodule(self): def pytest_xdist_getremotemodule(self):
@@ -283,7 +284,7 @@ class WorkerController:
self.channel.send((self.workerinput, args, option_dict, change_sys_path)) self.channel.send((self.workerinput, args, option_dict, change_sys_path))
if self.putevent: if self.putevent:
self.channel.setcallback(self.process_from_remote, endmarker=self.ENDMARK) self.channel.setcallback(self.process_from_remote, endmarker=Marker.END)
def ensure_teardown(self): def ensure_teardown(self):
if hasattr(self, "channel"): if hasattr(self, "channel"):
@@ -331,7 +332,7 @@ class WorkerController:
avoid raising exceptions or doing heavy work. avoid raising exceptions or doing heavy work.
""" """
try: try:
if eventcall == self.ENDMARK: if eventcall is Marker.END:
err = self.channel._getremoteerror() err = self.channel._getremoteerror()
if not self._down: if not self._down:
if not err or isinstance(err, EOFError): if not err or isinstance(err, EOFError):
@@ -374,16 +375,6 @@ class WorkerController:
nodeid=kwargs["nodeid"], nodeid=kwargs["nodeid"],
fslocation=kwargs["nodeid"], fslocation=kwargs["nodeid"],
) )
elif eventname == "warning_captured":
warning_message = unserialize_warning_message(
kwargs["warning_message_data"]
)
self.notify_inproc(
eventname,
warning_message=warning_message,
when=kwargs["when"],
item=kwargs["item"],
)
elif eventname == "warning_recorded": elif eventname == "warning_recorded":
warning_message = unserialize_warning_message( warning_message = unserialize_warning_message(
kwargs["warning_message_data"] kwargs["warning_message_data"]

View File

@@ -1,9 +1,8 @@
from __future__ import annotations
import os import os
import re import re
import shutil import shutil
from typing import Dict
from typing import List
from typing import Tuple
import pytest import pytest
@@ -1510,13 +1509,17 @@ class TestLocking:
""" + ((_test_content * 4) % ("A", "B", "C", "D")) """ + ((_test_content * 4) % ("A", "B", "C", "D"))
@pytest.mark.parametrize("scope", ["each", "load", "loadscope", "loadfile", "no"]) @pytest.mark.parametrize(
"scope", ["each", "load", "loadscope", "loadfile", "worksteal", "no"]
)
def test_single_file(self, pytester, scope) -> None: def test_single_file(self, pytester, scope) -> None:
pytester.makepyfile(test_a=self.test_file1) pytester.makepyfile(test_a=self.test_file1)
result = pytester.runpytest("-n2", "--dist=%s" % scope, "-v") result = pytester.runpytest("-n2", "--dist=%s" % scope, "-v")
result.assert_outcomes(passed=(12 if scope != "each" else 12 * 2)) result.assert_outcomes(passed=(12 if scope != "each" else 12 * 2))
@pytest.mark.parametrize("scope", ["each", "load", "loadscope", "loadfile", "no"]) @pytest.mark.parametrize(
"scope", ["each", "load", "loadscope", "loadfile", "worksteal", "no"]
)
def test_multi_file(self, pytester, scope) -> None: def test_multi_file(self, pytester, scope) -> None:
pytester.makepyfile( pytester.makepyfile(
test_a=self.test_file1, test_a=self.test_file1,
@@ -1528,7 +1531,7 @@ class TestLocking:
result.assert_outcomes(passed=(48 if scope != "each" else 48 * 2)) result.assert_outcomes(passed=(48 if scope != "each" else 48 * 2))
def parse_tests_and_workers_from_output(lines: List[str]) -> List[Tuple[str, str, str]]: def parse_tests_and_workers_from_output(lines: list[str]) -> list[tuple[str, str, str]]:
result = [] result = []
for line in lines: for line in lines:
# example match: "[gw0] PASSED test_a.py::test[7]" # example match: "[gw0] PASSED test_a.py::test[7]"
@@ -1550,9 +1553,9 @@ def parse_tests_and_workers_from_output(lines: List[str]) -> List[Tuple[str, str
def get_workers_and_test_count_by_prefix( def get_workers_and_test_count_by_prefix(
prefix: str, lines: List[str], expected_status: str = "PASSED" prefix: str, lines: list[str], expected_status: str = "PASSED"
) -> Dict[str, int]: ) -> dict[str, int]:
result: Dict[str, int] = {} result: dict[str, int] = {}
for worker, status, nodeid in parse_tests_and_workers_from_output(lines): for worker, status, nodeid in parse_tests_and_workers_from_output(lines):
if expected_status == status and nodeid.startswith(prefix): if expected_status == status and nodeid.startswith(prefix):
result[worker] = result.get(worker, 0) + 1 result[worker] = result.get(worker, 0) + 1

View File

@@ -1,5 +1,6 @@
from __future__ import annotations
import shutil import shutil
from typing import List
import execnet import execnet
import pytest import pytest
@@ -41,7 +42,7 @@ def specssh(request) -> str:
# configuration information for tests # configuration information for tests
def getgspecs(config) -> List[execnet.XSpec]: def getgspecs(config) -> list[execnet.XSpec]:
return [execnet.XSpec(spec) for spec in config.getvalueorskip("gspecs")] return [execnet.XSpec(spec) for spec in config.getvalueorskip("gspecs")]

View File

@@ -442,7 +442,7 @@ class TestDistReporter:
@pytest.mark.xfail @pytest.mark.xfail
def test_rsync_printing(self, pytester: pytest.Pytester, linecomp) -> None: def test_rsync_printing(self, pytester: pytest.Pytester, linecomp) -> None:
config = pytester.parseconfig() config = pytester.parseconfig()
from _pytest.pytest_terminal import TerminalReporter from _pytest.terminal import TerminalReporter
rep = TerminalReporter(config, file=linecomp.stringio) rep = TerminalReporter(config, file=linecomp.stringio)
config.pluginmanager.register(rep, "terminalreporter") config.pluginmanager.register(rep, "terminalreporter")

View File

@@ -1,9 +1,10 @@
from __future__ import annotations
import pathlib import pathlib
from pathlib import Path from pathlib import Path
import shutil import shutil
import tempfile import tempfile
import textwrap import textwrap
from typing import List
import unittest.mock import unittest.mock
import pytest import pytest
@@ -75,7 +76,7 @@ class TestStatRecorder:
# make check()'s visit() call return our just removed # make check()'s visit() call return our just removed
# path as if we were in a race condition # path as if we were in a race condition
dirname = str(tmp) dirname = str(tmp)
dirnames: List[str] = [] dirnames: list[str] = []
filenames = [str(p)] filenames = [str(p)]
with unittest.mock.patch( with unittest.mock.patch(
"os.walk", return_value=[(dirname, dirnames, filenames)], autospec=True "os.walk", return_value=[(dirname, dirnames, filenames)], autospec=True
@@ -122,7 +123,7 @@ class TestRemoteControl:
item = pytester.getitem("def test_func():\n assert 0\n") item = pytester.getitem("def test_func():\n assert 0\n")
control = RemoteControl(item.config) control = RemoteControl(item.config)
control.setup() control.setup()
failures = control.runsession() failures = control.runsession()[0]
assert failures assert failures
control.setup() control.setup()
item.path.write_text("def test_func():\n assert 1\n") item.path.write_text("def test_func():\n assert 1\n")