diff --git a/.taskcluster.yml b/.taskcluster.yml index a54ac62..fc068b0 100644 --- a/.taskcluster.yml +++ b/.taskcluster.yml @@ -20,10 +20,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: @@ -51,7 +47,7 @@ tasks: env: TOXENV: py312,lint - name: PyPI upload - version: "3.8" + version: "3.9" env: TOXENV: pypi script: diff --git a/pyproject.toml b/pyproject.toml index ad7e7fa..967a216 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -53,7 +53,7 @@ log_level = "DEBUG" [tool.ruff] fix = true -target-version = "py38" +target-version = "py39" [tool.ruff.lint] select = [ @@ -82,7 +82,6 @@ select = [ ] ignore = [ "PERF203", - "SIM117", ] [tool.setuptools_scm] diff --git a/setup.cfg b/setup.cfg index 9cef2e2..8853b30 100644 --- a/setup.cfg +++ b/setup.cfg @@ -25,7 +25,7 @@ package_dir = = src packages = ffpuppet -python_requires = >=3.8 +python_requires = >=3.9 zip_safe = False [options.entry_points] diff --git a/src/ffpuppet/checks.py b/src/ffpuppet/checks.py index 34d39aa..5daf567 100644 --- a/src/ffpuppet/checks.py +++ b/src/ffpuppet/checks.py @@ -7,10 +7,14 @@ from abc import ABC, abstractmethod from os import SEEK_SET, stat from platform import system -from typing import IO, Callable, Iterable, Pattern +from typing import IO, TYPE_CHECKING, Callable from psutil import AccessDenied, NoSuchProcess, Process +if TYPE_CHECKING: + from collections.abc import Iterable + from re import Pattern + __author__ = "Tyson Smith" __credits__ = ["Tyson Smith"] diff --git a/src/ffpuppet/core.py b/src/ffpuppet/core.py index e96e59d..464442d 100644 --- a/src/ffpuppet/core.py +++ b/src/ffpuppet/core.py @@ -12,13 +12,12 @@ from os.path import isfile, realpath from pathlib import Path from platform import system -from re import IGNORECASE +from re import IGNORECASE, Pattern from re import compile as re_compile from re import match as re_match from shutil import copy, copyfileobj from subprocess import Popen, check_output from sys import executable -from typing import Generator, Pattern from urllib.request import pathname2url with suppress(ImportError): @@ -30,6 +29,8 @@ with suppress(ImportError): from xvfbwrapper import Xvfb +from typing import TYPE_CHECKING + from .bootstrapper import Bootstrapper from .checks import CheckLogContents, CheckLogSize, CheckMemoryUsage from .exceptions import BrowserExecutionError, InvalidPrefs, LaunchError @@ -39,6 +40,9 @@ from .profile import Profile from .puppet_logger import PuppetLogger +if TYPE_CHECKING: + from collections.abc import Generator + if system() == "Windows": # config_job_object is only available on Windows from .job_object import config_job_object, resume_suspended_process diff --git a/src/ffpuppet/helpers.py b/src/ffpuppet/helpers.py index 0c057b7..4405480 100644 --- a/src/ffpuppet/helpers.py +++ b/src/ffpuppet/helpers.py @@ -12,12 +12,15 @@ from stat import S_IWUSR from subprocess import STDOUT, CalledProcessError, check_output from time import sleep, time -from typing import Any, Callable, Generator, Iterable +from typing import TYPE_CHECKING, Any, Callable from psutil import Process, process_iter from .sanitizer_util import SanitizerOptions +if TYPE_CHECKING: + from collections.abc import Generator, Iterable + if system() == "Windows": from .lsof import pids_by_file diff --git a/src/ffpuppet/minidump_parser.py b/src/ffpuppet/minidump_parser.py index c29a7f1..05f9296 100644 --- a/src/ffpuppet/minidump_parser.py +++ b/src/ffpuppet/minidump_parser.py @@ -168,32 +168,34 @@ def create_log(self, src: Path, filename: str, timeout: int = 300) -> Path: cmd = self._cmd(src) dst = self._storage / filename # using nested with statements for python 3.8 support - with TemporaryFile(dir=self._storage, prefix="mdsw_out_") as out_fp: - with TemporaryFile(dir=self._storage, prefix="mdsw_err_") as err_fp: - LOG.debug("running %r", " ".join(cmd)) - try: - run(cmd, check=True, stderr=err_fp, stdout=out_fp, timeout=timeout) - out_fp.seek(0) - # load json, format data and write log - with dst.open("wb") as log_fp: - self._fmt_output(load(out_fp), log_fp) - except (CalledProcessError, JSONDecodeError, TimeoutExpired) as exc: - if isinstance(exc, CalledProcessError): - msg = f"minidump-stackwalk failed ({exc.returncode})" - elif isinstance(exc, JSONDecodeError): - msg = "json decode error" - else: - msg = "minidump-stackwalk timeout" - LOG.warning("Failed to parse minidump: %s", msg) - err_fp.seek(0) - out_fp.seek(0) - # write log - with dst.open("wb") as log_fp: - log_fp.write(f"Failed to parse minidump: {msg}".encode()) - log_fp.write(b"\n\nminidump-stackwalk stderr:\n") - log_fp.write(err_fp.read()) - log_fp.write(b"\n\nminidump-stackwalk stdout:\n") - log_fp.write(out_fp.read()) + with ( + TemporaryFile(dir=self._storage, prefix="mdsw_out_") as out_fp, + TemporaryFile(dir=self._storage, prefix="mdsw_err_") as err_fp, + ): + LOG.debug("running %r", " ".join(cmd)) + try: + run(cmd, check=True, stderr=err_fp, stdout=out_fp, timeout=timeout) + out_fp.seek(0) + # load json, format data and write log + with dst.open("wb") as log_fp: + self._fmt_output(load(out_fp), log_fp) + except (CalledProcessError, JSONDecodeError, TimeoutExpired) as exc: + if isinstance(exc, CalledProcessError): + msg = f"minidump-stackwalk failed ({exc.returncode})" + elif isinstance(exc, JSONDecodeError): + msg = "json decode error" + else: + msg = "minidump-stackwalk timeout" + LOG.warning("Failed to parse minidump: %s", msg) + err_fp.seek(0) + out_fp.seek(0) + # write log + with dst.open("wb") as log_fp: + log_fp.write(f"Failed to parse minidump: {msg}".encode()) + log_fp.write(b"\n\nminidump-stackwalk stderr:\n") + log_fp.write(err_fp.read()) + log_fp.write(b"\n\nminidump-stackwalk stdout:\n") + log_fp.write(out_fp.read()) return dst @staticmethod diff --git a/src/ffpuppet/process_tree.py b/src/ffpuppet/process_tree.py index a2048f1..7dcae5e 100644 --- a/src/ffpuppet/process_tree.py +++ b/src/ffpuppet/process_tree.py @@ -10,7 +10,7 @@ from pathlib import Path from platform import system from time import perf_counter, sleep -from typing import TYPE_CHECKING, Callable, Generator, Iterable, List, Tuple, cast +from typing import TYPE_CHECKING, Callable, cast try: from signal import SIGUSR1, Signals @@ -24,6 +24,7 @@ from .exceptions import TerminateError if TYPE_CHECKING: + from collections.abc import Generator, Iterable from subprocess import Popen LOG = getLogger(__name__) @@ -64,9 +65,8 @@ def _safe_wait_procs( while True: remaining = None if deadline is None else max(deadline - perf_counter(), 0) with suppress(AccessDenied): - # Python 3.8 is not compatible with __future__.annotations in cast() return cast( - Tuple[List[Process], List[Process]], + tuple[list[Process], list[Process]], wait_procs(procs, timeout=remaining, callback=callback), ) if deadline is not None and deadline <= perf_counter(): diff --git a/src/ffpuppet/puppet_logger.py b/src/ffpuppet/puppet_logger.py index c1c3a5f..4ea6c3d 100644 --- a/src/ffpuppet/puppet_logger.py +++ b/src/ffpuppet/puppet_logger.py @@ -13,10 +13,13 @@ from shutil import copy2, copyfileobj, copytree, rmtree from subprocess import STDOUT, CalledProcessError, check_output from tempfile import NamedTemporaryFile, mkdtemp -from typing import IO, Iterator, KeysView +from typing import IO, TYPE_CHECKING from .helpers import onerror, warn_open +if TYPE_CHECKING: + from collections.abc import Iterator, KeysView + LOG = getLogger(__name__) __author__ = "Tyson Smith" @@ -278,11 +281,13 @@ def save_logs( # check logs for rr related issues # OSError: in case the file does not exist # ValueError: cannot mmap an empty file on Windows - with suppress(OSError, ValueError): - with (dest / "log_stderr.txt").open("rb") as lfp: - with mmap(lfp.fileno(), 0, access=ACCESS_READ) as lmm: - if lmm.find(b"=== Start rr backtrace:") != -1: - LOG.warning("rr traceback detected in stderr log") + with ( + suppress(OSError, ValueError), + (dest / "log_stderr.txt").open("rb") as lfp, + mmap(lfp.fileno(), 0, access=ACCESS_READ) as lmm, + ): + if lmm.find(b"=== Start rr backtrace:") != -1: + LOG.warning("rr traceback detected in stderr log") if rr_pack and not self._rr_packed: LOG.debug("packing rr trace") try: diff --git a/src/ffpuppet/sanitizer_util.py b/src/ffpuppet/sanitizer_util.py index a1dd719..a57e9b1 100644 --- a/src/ffpuppet/sanitizer_util.py +++ b/src/ffpuppet/sanitizer_util.py @@ -7,7 +7,10 @@ from logging import getLogger from os.path import exists from re import compile as re_compile -from typing import Iterator +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from collections.abc import Iterator LOG = getLogger(__name__) diff --git a/src/ffpuppet/test_bootstrapper.py b/src/ffpuppet/test_bootstrapper.py index 951aac8..36042c3 100644 --- a/src/ffpuppet/test_bootstrapper.py +++ b/src/ffpuppet/test_bootstrapper.py @@ -111,9 +111,11 @@ def test_bootstrapper_05(mocker): fake_conn.recv.return_value = "foo" fake_sock.accept.return_value = (fake_conn, None) mocker.patch("ffpuppet.bootstrapper.select", return_value=([fake_sock], None, None)) - with Bootstrapper(fake_sock) as bts: - with raises(BrowserTerminatedError, match="Failure during browser startup"): - bts.wait(lambda: False) + with ( + Bootstrapper(fake_sock) as bts, + raises(BrowserTerminatedError, match="Failure during browser startup"), + ): + bts.wait(lambda: False) assert fake_conn.close.call_count == 1 @@ -197,9 +199,8 @@ def test_bootstrapper_08(mocker, bind, attempts, raised): fake_sock.bind.side_effect = bind mocker.patch("ffpuppet.bootstrapper.select", return_value=([fake_sock], None, None)) mocker.patch("ffpuppet.bootstrapper.socket", return_value=fake_sock) - with raises(raised): - with Bootstrapper.create(attempts=attempts): - pass + with raises(raised), Bootstrapper.create(attempts=attempts): + pass assert fake_sock.bind.call_count == attempts assert fake_sock.close.call_count == attempts diff --git a/src/ffpuppet/test_ffpuppet.py b/src/ffpuppet/test_ffpuppet.py index 7d959d3..4f705bf 100644 --- a/src/ffpuppet/test_ffpuppet.py +++ b/src/ffpuppet/test_ffpuppet.py @@ -79,9 +79,8 @@ def _srv_thread(httpd): @mark.skipif(system() == "Windows", reason="Unsupported on Windows") def test_ffpuppet_00(tmp_path): """test that invalid executables raise the right exception""" - with FFPuppet() as ffp: - with raises(OSError, match="is not an executable"): - ffp.launch(tmp_path) + with FFPuppet() as ffp, raises(OSError, match="is not an executable"): + ffp.launch(tmp_path) def test_ffpuppet_01(): @@ -415,21 +414,22 @@ def test_ffpuppet_15(mocker, tmp_path, debugger, dbg_bin, version): def test_ffpuppet_16(tmp_path): """test calling save_logs() before close()""" - with FFPuppet() as ffp: - with HTTPTestServer() as srv: - ffp.launch(TESTFF_BIN, location=srv.get_addr()) - with raises(AssertionError): - ffp.save_logs(tmp_path / "logs") + with FFPuppet() as ffp, HTTPTestServer() as srv: + ffp.launch(TESTFF_BIN, location=srv.get_addr()) + with raises(AssertionError): + ffp.save_logs(tmp_path / "logs") def test_ffpuppet_17(tmp_path): """test detecting invalid prefs file""" prefs = tmp_path / "prefs.js" prefs.write_bytes(b"//fftest_invalid_js\n") - with FFPuppet() as ffp: - with HTTPTestServer() as srv: - with raises(LaunchError, match="'.+?' is invalid"): - ffp.launch(TESTFF_BIN, location=srv.get_addr(), prefs_js=prefs) + with ( + FFPuppet() as ffp, + HTTPTestServer() as srv, + raises(LaunchError, match="'.+?' is invalid"), + ): + ffp.launch(TESTFF_BIN, location=srv.get_addr(), prefs_js=prefs) def test_ffpuppet_18(): diff --git a/src/ffpuppet/test_profile.py b/src/ffpuppet/test_profile.py index 8a1cc8c..bc7a619 100644 --- a/src/ffpuppet/test_profile.py +++ b/src/ffpuppet/test_profile.py @@ -204,9 +204,11 @@ def test_profile_06(mocker, tmp_path): mocker.patch("ffpuppet.profile.rmtree", autospec=True, side_effect=OSError("test")) with Profile(working_path=str(tmp_path)) as profile: profile.remove(ignore_errors=True) - with Profile(working_path=str(tmp_path)) as profile: - with raises(OSError, match="test"): - profile.remove(ignore_errors=False) + with ( + Profile(working_path=str(tmp_path)) as profile, + raises(OSError, match="test"), + ): + profile.remove(ignore_errors=False) def test_profile_07(mocker, tmp_path): diff --git a/src/ffpuppet/test_puppet_logger.py b/src/ffpuppet/test_puppet_logger.py index 5360b14..b207b35 100644 --- a/src/ffpuppet/test_puppet_logger.py +++ b/src/ffpuppet/test_puppet_logger.py @@ -221,11 +221,13 @@ def test_puppet_logger_07(mocker, tmp_path): def test_puppet_logger_08(tmp_path): """test PuppetLogger.add_log() with file not on disk""" - with PuppetLogger(base_path=str(tmp_path)) as plog: - with SpooledTemporaryFile(max_size=2048) as log_fp: - plog.add_log("test", logfp=log_fp) - with raises(OSError, match="log file None does not exist"): - plog.get_fp("test") + with ( + PuppetLogger(base_path=str(tmp_path)) as plog, + SpooledTemporaryFile(max_size=2048) as log_fp, + ): + plog.add_log("test", logfp=log_fp) + with raises(OSError, match="log file None does not exist"): + plog.get_fp("test") def test_puppet_logger_09(mocker, tmp_path): diff --git a/tox.ini b/tox.ini index fa31408..2db076b 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