Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 47 additions & 0 deletions src/cantools/database/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,13 @@
"Node", "Signal", "dump_file", "load", "load_file", "load_string"]

import os
import traceback
import warnings
from contextlib import nullcontext
from typing import Any, TextIO
from xml.etree import ElementTree

import textparser

try:
import diskcache # type: ignore
Expand All @@ -26,6 +30,34 @@
UnsupportedDatabaseFormatError,
)

_FORMAT_PROBE_ERRORS: tuple[type[Exception], ...] = (
Error,
textparser.ParseError,
ElementTree.ParseError,
ValueError,
KeyError,
)

_UNEXPECTED_PROBE_ERROR = (
"probing for {database_format} raised {error}, which does not say whether "
"the string is in that format. The string was reported as an unsupported "
"database instead. Please open an issue at "
"https://github.com/cantools/cantools/issues with the traceback below.\n{traceback}"
)


def _warn_unexpected_probe_error(database_format: str, exc: Exception) -> None:
"""Warn that a probe failed for a reason unrelated to the format."""
warnings.warn(
_UNEXPECTED_PROBE_ERROR.format(
database_format=database_format,
error=type(exc).__name__,
traceback="".join(traceback.format_exception(exc)),
),
UserWarning,
stacklevel=2,
)


def _resolve_database_format_and_encoding(database_format: str | None,
encoding: str | None,
Expand Down Expand Up @@ -339,33 +371,48 @@ def load_can_database(fmt: str) -> can.Database:
if database_format in ['arxml', None]:
try:
return load_can_database('arxml')
except _FORMAT_PROBE_ERRORS as e:
e_arxml = e
except Exception as e:
_warn_unexpected_probe_error('arxml', e)
e_arxml = e

if database_format in ['dbc', None]:
try:
return load_can_database('dbc')
except _FORMAT_PROBE_ERRORS as e:
e_dbc = e
except Exception as e:
_warn_unexpected_probe_error('dbc', e)
e_dbc = e

if database_format in ['kcd', None]:
try:
return load_can_database('kcd')
except _FORMAT_PROBE_ERRORS as e:
e_kcd = e
except Exception as e:
_warn_unexpected_probe_error('kcd', e)
e_kcd = e

if database_format in ['sym', None]:
try:
return load_can_database('sym')
except _FORMAT_PROBE_ERRORS as e:
e_sym = e
except Exception as e:
_warn_unexpected_probe_error('sym', e)
e_sym = e

if database_format in ['cdd', None]:
try:
db = diagnostics.Database()
db.add_cdd_string(string)
return db
except _FORMAT_PROBE_ERRORS as e:
e_cdd = e
except Exception as e:
_warn_unexpected_probe_error('cdd', e)
e_cdd = e

if database_format is not None:
Expand Down
58 changes: 58 additions & 0 deletions tests/test_database.py
Original file line number Diff line number Diff line change
Expand Up @@ -2851,6 +2851,64 @@ def test_load_bad_format(self):
with self.assertRaises(UnsupportedDatabaseFormatError):
cantools.database.load(StringIO(''))

def test_load_string_warns_about_an_unrelated_error(self):
"""An error unrelated to the format is reported with its traceback."""
duplicate_message_dbc = (
'VERSION ""\n'
'\n'
'BU_: ECU\n'
'\n'
'BO_ 256 Msg: 8 ECU\n'
' SG_ Sig1 : 0|8@1+ (1,0) [0|0] "" ECU\n'
'\n'
'BO_ 256 Msg: 8 ECU\n'
' SG_ Sig2 : 8|8@1+ (1,0) [0|0] "" ECU\n'
)

# emit() raises OSError on the duplicate-message warning the loader logs
class FailingHandler(logging.Handler):
def emit(self, record):
raise OSError(5, 'Input/output error')

logger = logging.getLogger('cantools')

@andlaus andlaus Oct 21, 2026 •

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

a comment of how the "unexpected" exception is triggered would be good. AFAICS, you "misappropriate" logging.warn() to raise an OSError instead of logging? (it is a neat kludge, but it should IMO be explained in the code.)

handler = FailingHandler()
logger.addHandler(handler)
propagate = logger.propagate
logger.propagate = False

try:
with warnings.catch_warnings(record=True) as caught:
warnings.simplefilter('always')
with self.assertRaises(UnsupportedDatabaseFormatError):
cantools.database.load_string(duplicate_message_dbc,
strict=False)
finally:
logger.removeHandler(handler)
logger.propagate = propagate

reported = [str(w.message) for w in caught
if 'does not say whether' in str(w.message)]
self.assertEqual(len(reported), 1)
self.assertIn('probing for dbc raised OSError', reported[0])
self.assertIn('github.com/cantools/cantools/issues', reported[0])
self.assertIn('Input/output error', reported[0])

def test_load_string_does_not_warn_about_a_format_error(self):
"""A string which is simply not a database warns about nothing."""
with warnings.catch_warnings(record=True) as caught:
warnings.simplefilter('always')
with self.assertRaises(UnsupportedDatabaseFormatError):
cantools.database.load_string('not a database at all')

self.assertEqual([w for w in caught
if 'does not say whether' in str(w.message)], [])

def test_load_string_still_reports_a_bad_format(self):
"""Narrowing the probe must not stop genuine format failures being caught."""
for content in ['not a database at all', '', '\x00\x01\x02']:
with self.assertRaises(UnsupportedDatabaseFormatError):
cantools.database.load_string(content)

def test_add_bad_kcd_string(self):
db = cantools.database.Database()

Expand Down
Loading