Skip to content

Commit

Permalink
fix: avoid a SQLite error when async mode isn't available. #1646
Browse files Browse the repository at this point in the history
  • Loading branch information
nedbat committed Jun 22, 2023
1 parent a5f37e0 commit 2125136
Show file tree
Hide file tree
Showing 5 changed files with 43 additions and 12 deletions.
6 changes: 6 additions & 0 deletions CHANGES.rst
Expand Up @@ -22,6 +22,11 @@ Unreleased

- Dropped support for Python 3.7.

- Fix: in unusual circumstances, SQLite cannot be set to asynchronous mode.
Coverage.py would fail with the error ``Safety level may not be changed
inside a transaction.`` This is now avoided, closing `issue 1646`_. Thanks
to Michael Bell for the detailed bug report.

- Docs: examples of configuration files now include separate examples for the
different syntaxes: .coveragerc, pyproject.toml, setup.cfg, and tox.ini.

Expand All @@ -32,6 +37,7 @@ Unreleased
- Added a CITATION.cff file, thanks to `Ken Schackart <pull 1641_>`_.

.. _pull 1641: https://github.com/nedbat/coveragepy/pull/1641
.. _issue 1646: https://github.com/nedbat/coveragepy/issues/1646


.. scriv-start-here
Expand Down
1 change: 1 addition & 0 deletions CONTRIBUTORS.txt
Expand Up @@ -147,6 +147,7 @@ Matthew Desmarais
Matus Valo
Max Linke
Mayank Singhal
Michael Bell
Michael Krebs
Michał Bultrowicz
Michał Górny
Expand Down
7 changes: 7 additions & 0 deletions coverage/debug.py
Expand Up @@ -15,6 +15,7 @@
import re
import reprlib
import sys
import traceback
import types
import _thread

Expand Down Expand Up @@ -172,6 +173,12 @@ def write_formatted_info(
write(f" {line}")


def exc_one_line(exc: Exception) -> str:
"""Get a one-line summary of an exception, including class name and message."""
lines = traceback.format_exception_only(type(exc), exc)
return "|".join(l.rstrip() for l in lines)


def short_stack(limit: Optional[int] = None, skip: int = 0) -> str:
"""Return a string summarizing the call stack.
Expand Down
29 changes: 19 additions & 10 deletions coverage/sqldata.py
Expand Up @@ -26,7 +26,7 @@
Optional, Sequence, Set, Tuple, TypeVar, Union,
)

from coverage.debug import NoDebugging, AutoReprMixin, clipped_repr
from coverage.debug import NoDebugging, AutoReprMixin, clipped_repr, exc_one_line
from coverage.exceptions import CoverageException, DataError
from coverage.files import PathAliases
from coverage.misc import file_be_gone, isolate_module
Expand Down Expand Up @@ -1130,11 +1130,11 @@ def _connect(self) -> None:
self.con.create_function("REGEXP", 2, lambda txt, pat: re.search(txt, pat) is not None)

# This pragma makes writing faster. It disables rollbacks, but we never need them.
# PyPy needs the .close() calls here, or sqlite gets twisted up:
# https://bitbucket.org/pypy/pypy/issues/2872/default-isolation-mode-is-different-on
self.execute_void("pragma journal_mode=off")
# This pragma makes writing faster.
self.execute_void("pragma synchronous=off")
# This pragma makes writing faster. It can fail in unusual situations
# (https://github.com/nedbat/coveragepy/issues/1646), so use fail_ok=True
# to keep things going.
self.execute_void("pragma synchronous=off", fail_ok=True)

def close(self) -> None:
"""If needed, close the connection."""
Expand All @@ -1159,7 +1159,7 @@ def __exit__(self, exc_type, exc_value, traceback) -> None: # type: ignore[n
self.close()
except Exception as exc:
if self.debug.should("sql"):
self.debug.write(f"EXCEPTION from __exit__: {exc}")
self.debug.write(f"EXCEPTION from __exit__: {exc_one_line(exc)}")
raise DataError(f"Couldn't end data file {self.filename!r}: {exc}") from exc

def _execute(self, sql: str, parameters: Iterable[Any]) -> sqlite3.Cursor:
Expand Down Expand Up @@ -1191,7 +1191,7 @@ def _execute(self, sql: str, parameters: Iterable[Any]) -> sqlite3.Cursor:
except Exception: # pragma: cant happen
pass
if self.debug.should("sql"):
self.debug.write(f"EXCEPTION from execute: {msg}")
self.debug.write(f"EXCEPTION from execute: {exc_one_line(exc)}")
raise DataError(f"Couldn't use data file {self.filename!r}: {msg}") from exc

@contextlib.contextmanager
Expand All @@ -1210,9 +1210,18 @@ def execute(
finally:
cur.close()

def execute_void(self, sql: str, parameters: Iterable[Any] = ()) -> None:
"""Same as :meth:`python:sqlite3.Connection.execute` when you don't need the cursor."""
self._execute(sql, parameters).close()
def execute_void(self, sql: str, parameters: Iterable[Any] = (), fail_ok: bool = False) -> None:
"""Same as :meth:`python:sqlite3.Connection.execute` when you don't need the cursor.
If `fail_ok` is True, then SQLite errors are ignored.
"""
try:
# PyPy needs the .close() calls here, or sqlite gets twisted up:
# https://bitbucket.org/pypy/pypy/issues/2872/default-isolation-mode-is-different-on
self._execute(sql, parameters).close()
except DataError:
if not fail_ok:
raise

def execute_for_rowid(self, sql: str, parameters: Iterable[Any] = ()) -> int:
"""Like execute, but returns the lastrowid."""
Expand Down
12 changes: 10 additions & 2 deletions tests/test_debug.py
Expand Up @@ -19,9 +19,10 @@
from coverage import env
from coverage.debug import (
DebugOutputFile,
clipped_repr, filter_text, info_formatter, info_header, relevant_environment_display,
short_id, short_stack,
clipped_repr, exc_one_line, filter_text, info_formatter, info_header,
relevant_environment_display, short_id, short_stack,
)
from coverage.exceptions import DataError

from tests.coveragetest import CoverageTest
from tests.helpers import re_line, re_lines, re_lines_text
Expand Down Expand Up @@ -317,3 +318,10 @@ def test_relevant_environment_display() -> None:
("SOME_PYOTHER", "xyz123"),
("TMP", "temporary"),
]


def test_exc_one_line() -> None:
try:
raise DataError("wtf?")
except Exception as exc:
assert exc_one_line(exc) == "coverage.exceptions.DataError: wtf?"

0 comments on commit 2125136

Please sign in to comment.