From 7ebfc533efdd268d3651bf47a436c76ae70ea8a9 Mon Sep 17 00:00:00 2001 From: Tyson Smith Date: Thu, 31 Oct 2024 10:23:26 -0700 Subject: [PATCH] build!: drop support for python 3.8 --- .taskcluster.yml | 6 +- grizzly/adapter/adapter.py | 6 +- grizzly/args.py | 5 +- grizzly/common/bugzilla.py | 5 +- grizzly/common/fuzzmanager.py | 7 +- grizzly/common/status.py | 3 +- grizzly/common/status_reporter.py | 25 ++++--- grizzly/common/storage.py | 7 +- grizzly/common/test_fuzzmanager.py | 5 +- grizzly/common/test_status_reporter.py | 9 ++- grizzly/common/utils.py | 5 +- grizzly/reduce/strategies/__init__.py | 6 +- grizzly/reduce/strategies/beautify.py | 4 +- grizzly/reduce/strategies/lithium.py | 3 +- grizzly/reduce/strategies/testcases.py | 5 +- grizzly/reduce/test_reduce.py | 5 +- grizzly/replay/test_replay.py | 96 ++++++++++++++------------ grizzly/target/puppet_target.py | 7 +- grizzly/test_session.py | 8 ++- pyproject.toml | 3 +- sapphire/core.py | 3 +- sapphire/job.py | 6 +- sapphire/test_connection_manager.py | 8 ++- setup.cfg | 2 +- tox.ini | 2 +- 25 files changed, 140 insertions(+), 101 deletions(-) diff --git a/.taskcluster.yml b/.taskcluster.yml index 886c219b..0329a534 100644 --- a/.taskcluster.yml +++ b/.taskcluster.yml @@ -48,10 +48,6 @@ tasks: - tox; tox -e codecov jobs: include: - - name: tests python 3.8 - version: "3.8" - env: - TOXENV: py38,lint - name: tests python 3.9 version: "3.9" env: @@ -79,7 +75,7 @@ tasks: env: TOXENV: py312,lint - name: PyPI upload - version: "3.8" + version: "3.9" env: TOXENV: pypi script: diff --git a/grizzly/adapter/adapter.py b/grizzly/adapter/adapter.py index 8c7e62bb..e159279b 100644 --- a/grizzly/adapter/adapter.py +++ b/grizzly/adapter/adapter.py @@ -5,11 +5,13 @@ from abc import ABCMeta, abstractmethod from pathlib import Path -from typing import TYPE_CHECKING, Any, Generator, final +from typing import TYPE_CHECKING, Any, final from ..common.utils import DEFAULT_TIME_LIMIT, HARNESS_FILE if TYPE_CHECKING: + from collections.abc import Generator + from sapphire import ServerMap from ..common.storage import TestCase @@ -187,7 +189,7 @@ def pre_launch(self) -> None: None """ - # TODO: update input_path type (str -> Path) + # TODO: update input_path type (str -> list[str]) def setup(self, input_path: str | None, server_map: ServerMap) -> None: """Optional. Automatically called once at startup. diff --git a/grizzly/args.py b/grizzly/args.py index 8dac2709..3da05c97 100644 --- a/grizzly/args.py +++ b/grizzly/args.py @@ -16,7 +16,7 @@ from pathlib import Path from platform import system from types import MappingProxyType -from typing import Iterable +from typing import TYPE_CHECKING from FTB.ProgramConfiguration import ProgramConfiguration @@ -24,6 +24,9 @@ from .common.plugins import scan_plugins, scan_target_assets from .common.utils import DEFAULT_TIME_LIMIT, TIMEOUT_DELAY, package_version +if TYPE_CHECKING: + from collections.abc import Iterable + # ref: https://stackoverflow.com/questions/12268602/sort-argparse-help-alphabetically class SortingHelpFormatter(HelpFormatter): diff --git a/grizzly/common/bugzilla.py b/grizzly/common/bugzilla.py index e63ab183..164d2f88 100644 --- a/grizzly/common/bugzilla.py +++ b/grizzly/common/bugzilla.py @@ -10,7 +10,7 @@ from pathlib import Path from shutil import rmtree from tempfile import mkdtemp -from typing import Generator +from typing import TYPE_CHECKING from zipfile import ZipFile from bugsy import Bug, Bugsy @@ -19,6 +19,9 @@ from .utils import grz_tmp +if TYPE_CHECKING: + from collections.abc import Generator + # attachments that can be ignored IGNORE_EXTS = frozenset({"c", "cpp", "diff", "exe", "log", "patch", "php", "py", "txt"}) # TODO: support all target assets diff --git a/grizzly/common/fuzzmanager.py b/grizzly/common/fuzzmanager.py index 6317476d..e4812b49 100644 --- a/grizzly/common/fuzzmanager.py +++ b/grizzly/common/fuzzmanager.py @@ -11,7 +11,7 @@ from re import search from shutil import copyfileobj, rmtree from tempfile import NamedTemporaryFile, mkdtemp -from typing import Any, Dict, Generator, cast +from typing import TYPE_CHECKING, Any, cast from zipfile import BadZipFile, ZipFile from Collector.Collector import Collector @@ -22,6 +22,9 @@ from .storage import TEST_INFO from .utils import grz_tmp +if TYPE_CHECKING: + from collections.abc import Generator + FM_CONFIG = Path.home() / ".fuzzmanagerconf" LOG = getLogger(__name__) @@ -79,7 +82,7 @@ def __getattr__(self, name: str) -> Any: need_raw = "1" if name in self.RAW_FIELDS else "0" # TODO: handle 403 and 404? self._data = cast( - Dict[str, Any], + dict[str, Any], self._coll.get(self._url, params={"include_raw": need_raw}).json(), ) if name not in self._data: diff --git a/grizzly/common/status.py b/grizzly/common/status.py index feb3bd60..e599cf6a 100644 --- a/grizzly/common/status.py +++ b/grizzly/common/status.py @@ -14,11 +14,12 @@ from os import getpid from sqlite3 import Connection, OperationalError, connect from time import perf_counter, time -from typing import TYPE_CHECKING, Any, Callable, Generator, cast +from typing import TYPE_CHECKING, Any, Callable, cast from .utils import grz_tmp if TYPE_CHECKING: + from collections.abc import Generator from pathlib import Path from .reporter import FuzzManagerReporter diff --git a/grizzly/common/status_reporter.py b/grizzly/common/status_reporter.py index 3380dcf9..f24ec4c2 100644 --- a/grizzly/common/status_reporter.py +++ b/grizzly/common/status_reporter.py @@ -19,7 +19,7 @@ from platform import system from re import match from time import gmtime, localtime, strftime -from typing import Callable, Generator, Iterable +from typing import TYPE_CHECKING, Callable from psutil import cpu_count, cpu_percent, disk_usage, getloadavg, virtual_memory @@ -32,6 +32,9 @@ ReductionStep, ) +if TYPE_CHECKING: + from collections.abc import Generator, Iterable + __all__ = ("StatusReporter",) __author__ = "Tyson Smith" __credits__ = ["Tyson Smith"] @@ -77,15 +80,17 @@ def from_file( token = b"Traceback (most recent call last):" assert len(token) < cls.READ_LIMIT try: - with log_file.open("rb") as lfp: - with mmap(lfp.fileno(), 0, access=ACCESS_READ) as lmm: - idx = lmm.find(token) - if idx == -1: - # no traceback here, move along - return None - # seek back 2KB to collect preceding lines - lmm.seek(max(idx - len(token) - 2048, 0)) - data = lmm.read(cls.READ_LIMIT) + with ( + log_file.open("rb") as lfp, + mmap(lfp.fileno(), 0, access=ACCESS_READ) as lmm, + ): + idx = lmm.find(token) + if idx == -1: + # no traceback here, move along + return None + # seek back 2KB to collect preceding lines + lmm.seek(max(idx - len(token) - 2048, 0)) + data = lmm.read(cls.READ_LIMIT) except (OSError, ValueError): # pragma: no cover # OSError: in case the file goes away # ValueError: cannot mmap an empty file on Windows diff --git a/grizzly/common/storage.py b/grizzly/common/storage.py index 2ca5f27e..fe9adfcc 100644 --- a/grizzly/common/storage.py +++ b/grizzly/common/storage.py @@ -12,10 +12,13 @@ from shutil import copyfile, copytree, move, rmtree from tempfile import NamedTemporaryFile, mkdtemp from time import time -from typing import Any, Generator, Iterator, cast +from typing import TYPE_CHECKING, Any, cast from .utils import grz_tmp, package_version +if TYPE_CHECKING: + from collections.abc import Generator, Iterator + __all__ = ("TestCase", "TestCaseLoadFailure", "TestFileExists") __author__ = "Tyson Smith" __credits__ = ["Tyson Smith"] @@ -35,7 +38,7 @@ class TestFileExists(Exception): TestFile with the same name""" -@dataclass(eq=False) +@dataclass(eq=False, frozen=True) class TestFileMap: optional: dict[str, Path] = field(default_factory=dict) required: dict[str, Path] = field(default_factory=dict) diff --git a/grizzly/common/test_fuzzmanager.py b/grizzly/common/test_fuzzmanager.py index db764528..562da7af 100644 --- a/grizzly/common/test_fuzzmanager.py +++ b/grizzly/common/test_fuzzmanager.py @@ -303,9 +303,8 @@ def test_crash_06(mocker): "rawStderr": "", "rawCrashData": "", } - with raises(RuntimeError) as exc: - with CrashEntry(123) as crash: - crash.create_signature(None) + with raises(RuntimeError) as exc, CrashEntry(123) as crash: + crash.create_signature(None) assert "insufficient data to generate" in str(exc).lower() assert coll.return_value.get.call_count == 1 diff --git a/grizzly/common/test_status_reporter.py b/grizzly/common/test_status_reporter.py index 10f7d9ad..a7b7550d 100644 --- a/grizzly/common/test_status_reporter.py +++ b/grizzly/common/test_status_reporter.py @@ -143,11 +143,10 @@ def test_reduce_status_reporter_04(mocker, tmp_path): status.run_params["splines"] = "reticulated" status.last_reports.append(45678) status.record("init") - with status.measure("total"): - with status.measure("strategy_0"): - status.iterations = 1 - status.attempts = 1 - status.successes = 1 + with status.measure("total"), status.measure("strategy_0"): + status.iterations = 1 + status.attempts = 1 + status.successes = 1 status.report(force=True) rptr = ReductionStatusReporter.load(db_file) assert rptr.reports diff --git a/grizzly/common/utils.py b/grizzly/common/utils.py index 673d2d4d..aa2fe15c 100644 --- a/grizzly/common/utils.py +++ b/grizzly/common/utils.py @@ -10,7 +10,10 @@ from os import getenv from pathlib import Path from tempfile import gettempdir -from typing import Any, Generator, Iterable +from typing import TYPE_CHECKING, Any + +if TYPE_CHECKING: + from collections.abc import Generator, Iterable __all__ = ( "ConfigError", diff --git a/grizzly/reduce/strategies/__init__.py b/grizzly/reduce/strategies/__init__.py index fe158a8a..1e34ba62 100644 --- a/grizzly/reduce/strategies/__init__.py +++ b/grizzly/reduce/strategies/__init__.py @@ -19,11 +19,13 @@ from pathlib import Path from shutil import rmtree from tempfile import mkdtemp -from typing import TYPE_CHECKING, Iterable, Iterator, Type, cast +from typing import TYPE_CHECKING, cast from ...common.utils import grz_tmp if TYPE_CHECKING: + from collections.abc import Iterable, Iterator + from ...common.storage import TestCase try: @@ -202,7 +204,7 @@ def _load_strategies() -> dict[str, type[Strategy]]: strategies: dict[str, type[Strategy]] = {} for entry_point in iter_entry_points("grizzly_reduce_strategies"): try: - strategy_cls = cast(Type[Strategy], entry_point.load()) + strategy_cls = cast(type[Strategy], entry_point.load()) assert ( strategy_cls.name == entry_point.name ), f"entry_point name mismatch, check setup.py and {strategy_cls.__name__}" diff --git a/grizzly/reduce/strategies/beautify.py b/grizzly/reduce/strategies/beautify.py index 5ea57e5e..7f116a89 100644 --- a/grizzly/reduce/strategies/beautify.py +++ b/grizzly/reduce/strategies/beautify.py @@ -12,7 +12,8 @@ import re from abc import ABC, abstractmethod from logging import getLogger -from typing import TYPE_CHECKING, Generator, Match, cast +from re import Match +from typing import TYPE_CHECKING, cast from lithium.testcases import TestcaseLine @@ -34,6 +35,7 @@ from . import Strategy, _contains_dd if TYPE_CHECKING: + from collections.abc import Generator from pathlib import Path LOG = getLogger(__name__) diff --git a/grizzly/reduce/strategies/lithium.py b/grizzly/reduce/strategies/lithium.py index 90ea474e..1bee269f 100644 --- a/grizzly/reduce/strategies/lithium.py +++ b/grizzly/reduce/strategies/lithium.py @@ -6,7 +6,7 @@ from abc import ABC from logging import getLogger -from typing import TYPE_CHECKING, Iterator +from typing import TYPE_CHECKING from lithium.strategies import CheckOnly, Minimize, ReductionIterator from lithium.strategies import CollapseEmptyBraces as LithCollapseEmptyBraces @@ -18,6 +18,7 @@ from . import Strategy, _contains_dd if TYPE_CHECKING: + from collections.abc import Iterator from pathlib import Path LOG = getLogger(__name__) diff --git a/grizzly/reduce/strategies/testcases.py b/grizzly/reduce/strategies/testcases.py index c3616664..40bd6882 100644 --- a/grizzly/reduce/strategies/testcases.py +++ b/grizzly/reduce/strategies/testcases.py @@ -6,11 +6,14 @@ from logging import getLogger from shutil import rmtree -from typing import Generator +from typing import TYPE_CHECKING from ...common.storage import TestCase from . import Strategy +if TYPE_CHECKING: + from collections.abc import Generator + LOG = getLogger(__name__) diff --git a/grizzly/reduce/test_reduce.py b/grizzly/reduce/test_reduce.py index f0f04072..de03b606 100644 --- a/grizzly/reduce/test_reduce.py +++ b/grizzly/reduce/test_reduce.py @@ -734,9 +734,8 @@ def fake_iter(): ["fake"], log_path, use_analysis=False, - ) as mgr: - with raises(KeyboardInterrupt): - mgr.run() + ) as mgr, raises(KeyboardInterrupt): + mgr.run() n_reports = 1 reports = {"20"} diff --git a/grizzly/replay/test_replay.py b/grizzly/replay/test_replay.py index 94ab2a38..1be5ccb3 100644 --- a/grizzly/replay/test_replay.py +++ b/grizzly/replay/test_replay.py @@ -41,18 +41,20 @@ def test_replay_01(mocker, server, tmp_path, post_launch_delay): target.monitor.is_healthy.return_value = False (tmp_path / "a.html").touch() server.serve_path.return_value = (Served.ALL, {"a.html": str(tmp_path / "a.html")}) - with TestCase.load(tmp_path) as testcase: - with ReplayManager([], server, target, use_harness=True, relaunch=1) as replay: - assert not replay.run([testcase], 10, post_launch_delay=post_launch_delay) - assert replay.signature is None - assert replay.status - assert replay.status.ignored == 0 - assert replay.status.iteration == 1 - assert replay.status.results.total == 0 - assert target.monitor.is_healthy.call_count == 1 - assert target.close.call_count == 2 - assert target.close.mock_calls[0] == mocker.call() - assert target.close.mock_calls[1] == mocker.call(force_close=True) + with ( + TestCase.load(tmp_path) as testcase, + ReplayManager([], server, target, use_harness=True, relaunch=1) as replay, + ): + assert not replay.run([testcase], 10, post_launch_delay=post_launch_delay) + assert replay.signature is None + assert replay.status + assert replay.status.ignored == 0 + assert replay.status.iteration == 1 + assert replay.status.results.total == 0 + assert target.monitor.is_healthy.call_count == 1 + assert target.close.call_count == 2 + assert target.close.mock_calls[0] == mocker.call() + assert target.close.mock_calls[1] == mocker.call(force_close=True) def test_replay_02(mocker, server, tmp_path): @@ -65,17 +67,19 @@ def test_replay_02(mocker, server, tmp_path): iter_cb = mocker.Mock() (tmp_path / "a.html").touch() server.serve_path.return_value = (Served.ALL, {"a.html": str(tmp_path / "a.html")}) - with TestCase.load(tmp_path) as testcase: - with ReplayManager([], server, target, use_harness=True, relaunch=20) as replay: - assert not replay.run([testcase], 10, repeat=10, on_iteration_cb=iter_cb) - assert replay.signature is None - assert replay.status - assert replay.status.ignored == 0 - assert replay.status.iteration == iter_cb.call_count == 10 - assert replay.status.results.total == 0 - assert target.handle_hang.call_count == 0 - assert target.monitor.is_healthy.call_count == 1 - assert target.close.call_count == 2 + with ( + TestCase.load(tmp_path) as testcase, + ReplayManager([], server, target, use_harness=True, relaunch=20) as replay, + ): + assert not replay.run([testcase], 10, repeat=10, on_iteration_cb=iter_cb) + assert replay.signature is None + assert replay.status + assert replay.status.ignored == 0 + assert replay.status.iteration == iter_cb.call_count == 10 + assert replay.status.results.total == 0 + assert target.handle_hang.call_count == 0 + assert target.monitor.is_healthy.call_count == 1 + assert target.close.call_count == 2 def test_replay_03(mocker, server, tmp_path): @@ -91,16 +95,18 @@ def test_replay_03(mocker, server, tmp_path): target.check_result.return_value = Result.NONE (tmp_path / "a.html").touch() server.serve_path.return_value = (Served.ALL, {"a.html": str(tmp_path / "a.html")}) - with TestCase.load(tmp_path) as testcase: - with ReplayManager([], server, target, use_harness=True, relaunch=20) as replay: - assert not replay.run([testcase], 10, repeat=10, min_results=1) - assert replay.status - assert replay.status.ignored == 0 - assert replay.status.iteration == 10 - assert replay.status.results.total == 0 - assert target.handle_hang.call_count == 0 - assert target.monitor.is_healthy.call_count == 0 - assert target.close.call_count == 1 + with ( + TestCase.load(tmp_path) as testcase, + ReplayManager([], server, target, use_harness=True, relaunch=20) as replay, + ): + assert not replay.run([testcase], 10, repeat=10, min_results=1) + assert replay.status + assert replay.status.ignored == 0 + assert replay.status.iteration == 10 + assert replay.status.results.total == 0 + assert target.handle_hang.call_count == 0 + assert target.monitor.is_healthy.call_count == 0 + assert target.close.call_count == 1 @mark.parametrize("sig_parsed", [True, False]) @@ -593,17 +599,19 @@ def test_replay_19(mocker, server, tmp_path): Served.ALL, {"test.html": str(tmp_path / "test.html")}, ) - with TestCase.load(tmp_path) as testcase: - with ReplayManager([], server, target, use_harness=True) as replay: - assert not replay.run([testcase], 30) - assert replay.status - assert replay.status.iteration == 1 - assert not replay.run([testcase], 30) - assert replay.status - assert replay.status.iteration == 1 - assert not replay.run([testcase], 30) - assert replay.status - assert replay.status.iteration == 1 + with ( + TestCase.load(tmp_path) as testcase, + ReplayManager([], server, target, use_harness=True) as replay, + ): + assert not replay.run([testcase], 30) + assert replay.status + assert replay.status.iteration == 1 + assert not replay.run([testcase], 30) + assert replay.status + assert replay.status.iteration == 1 + assert not replay.run([testcase], 30) + assert replay.status + assert replay.status.iteration == 1 assert server.serve_path.call_count == 3 diff --git a/grizzly/target/puppet_target.py b/grizzly/target/puppet_target.py index a6cb4517..fbc498dd 100644 --- a/grizzly/target/puppet_target.py +++ b/grizzly/target/puppet_target.py @@ -9,7 +9,7 @@ from platform import system from signal import SIGABRT from tempfile import TemporaryDirectory, mkdtemp -from typing import Any, Dict, Optional, cast +from typing import Any, Optional, cast from ffpuppet import BrowserTimeoutError, Debugger, FFPuppet, LaunchError, Reason from ffpuppet.helpers import certutil_available, certutil_find @@ -95,6 +95,7 @@ def __init__( **kwds: dict[str, Any], ) -> None: LOG.debug("ffpuppet version: %s", package_version("ffpuppet")) + # Optional is required for Python 3.9 certs = cast(Optional[CertificateBundle], kwds.pop("certs", None)) # only pass certs to FFPuppet if certutil is available # otherwise certs can't be used @@ -126,6 +127,7 @@ def __init__( # create Puppet object self._puppet = FFPuppet( debugger=self._debugger, + # Optional is required for Python 3.9 headless=cast(Optional[str], kwds.pop("headless", None)), working_path=str(grz_tmp("target")), ) @@ -259,7 +261,8 @@ def launch(self, location: str) -> None: memory_limit=self.memory_limit, prefs_js=self._prefs, extension=[self._extension] if self._extension else None, - env_mod=cast(Dict[str, Optional[str]], env_mod), + # Optional is required for Python 3.9 + env_mod=cast(dict[str, Optional[str]], env_mod), cert_files=[self.certs.root] if self.certs else None, ) except LaunchError as exc: diff --git a/grizzly/test_session.py b/grizzly/test_session.py index c7f4ab5b..4ae4fcaa 100644 --- a/grizzly/test_session.py +++ b/grizzly/test_session.py @@ -233,9 +233,11 @@ def generate(self, _testcase, _server_map): server.serve_path.return_value = (Served.NONE, {}) target = mocker.Mock(spec_set=Target, launch_timeout=30) target.monitor.launches = 1 - with Session(FuzzAdapter("fuzz"), None, server, target) as session: - with raises(SessionError, match="Test case is missing entry point"): - session.run([], 10) + with ( + Session(FuzzAdapter("fuzz"), None, server, target) as session, + raises(SessionError, match="Test case is missing entry point"), + ): + session.run([], 10) @mark.parametrize( diff --git a/pyproject.toml b/pyproject.toml index 00d5586a..45220775 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -62,7 +62,7 @@ log_level = "DEBUG" [tool.ruff] fix = true -target-version = "py38" +target-version = "py39" [tool.ruff.lint] select = [ @@ -89,6 +89,5 @@ select = [ # pycodestyle "W", ] -ignore = ["SIM117"] [tool.setuptools_scm] diff --git a/sapphire/core.py b/sapphire/core.py index 1ac4610d..08889aac 100644 --- a/sapphire/core.py +++ b/sapphire/core.py @@ -11,13 +11,14 @@ from socket import SO_REUSEADDR, SOL_SOCKET, gethostname, socket from ssl import PROTOCOL_TLS_SERVER, SSLContext, SSLSocket from time import perf_counter, sleep -from typing import TYPE_CHECKING, Callable, Iterable, Mapping, cast +from typing import TYPE_CHECKING, Callable, cast from .connection_manager import ConnectionManager from .job import Job, Served if TYPE_CHECKING: from argparse import Namespace + from collections.abc import Iterable, Mapping from .certificate_bundle import CertificateBundle from .server_map import ServerMap diff --git a/sapphire/job.py b/sapphire/job.py index 5ee65922..0414c3ea 100644 --- a/sapphire/job.py +++ b/sapphire/job.py @@ -6,6 +6,7 @@ """ from __future__ import annotations +from collections.abc import Iterable, Mapping from enum import IntEnum, unique from errno import ENAMETOOLONG from itertools import chain @@ -15,7 +16,7 @@ from queue import Queue from threading import Event, Lock from types import MappingProxyType -from typing import TYPE_CHECKING, Any, Iterable, Mapping, NamedTuple, Tuple, Union, cast +from typing import TYPE_CHECKING, Any, NamedTuple, Union, cast from .server_map import DynamicResource, FileResource, RedirectResource, ServerMap @@ -93,7 +94,6 @@ def __init__( self.accepting = Event() self.accepting.set() self.auto_close = auto_close - # quotes around type for Python 3.8 self.exceptions: Queue[tuple[Any, Any, Any]] = Queue() self.forever = forever self.server_map = server_map @@ -127,7 +127,7 @@ def _build_pending(self, required_files: Iterable[str] | None) -> None: raise OSError(f"wwwroot '{self._wwwroot}' does not exist") if self.server_map: for url, resource in cast( - Iterable[Tuple[str, Union[DynamicResource, RedirectResource]]], + Iterable[tuple[str, Union[DynamicResource, RedirectResource]]], chain( self.server_map.redirect.items(), self.server_map.dynamic.items() ), diff --git a/sapphire/test_connection_manager.py b/sapphire/test_connection_manager.py index cab5b0bc..616fc01b 100644 --- a/sapphire/test_connection_manager.py +++ b/sapphire/test_connection_manager.py @@ -70,9 +70,11 @@ def test_connection_manager_03(mocker, tmp_path): serv_sock = mocker.Mock(spec_set=socket) serv_sock.accept.return_value = (clnt_sock, None) mocker.patch("sapphire.worker.select", return_value=([serv_sock], None, None)) - with raises(Exception, match="worker exception"): - with ConnectionManager(job, serv_sock) as mgr: - mgr.serve(10) + with ( + raises(Exception, match="worker exception"), + ConnectionManager(job, serv_sock) as mgr, + ): + mgr.serve(10) assert clnt_sock.close.call_count == 1 assert job.is_complete() assert job.exceptions.empty() diff --git a/setup.cfg b/setup.cfg index 4fee5c18..6f184ebd 100644 --- a/setup.cfg +++ b/setup.cfg @@ -39,7 +39,7 @@ packages = grizzly.target loki sapphire -python_requires = >=3.8 +python_requires = >=3.9 zip_safe = False [options.entry_points] diff --git a/tox.ini b/tox.ini index 73af1284..f8975761 100644 --- a/tox.ini +++ b/tox.ini @@ -1,5 +1,5 @@ [tox] -envlist = py{38,39,310,311,312},lint +envlist = py{39,310,311,312},lint skip_missing_interpreters = true tox_pip_extensions_ext_venv_update = true