From 72594f1482df197b5fe2548cfef1ad24da2f9674 Mon Sep 17 00:00:00 2001 From: eeshsaxena Date: Mon, 17 Aug 2026 11:25:01 +0530 Subject: [PATCH] Make InvalidNameError copyable and picklable InvalidNameError takes name and reason, but its base only stores the formatted message in args, so copy.deepcopy and pickle rebuild it with a single argument and raise TypeError. That surfaces through normal parsing: a failed name is kept in a MiddlewareErrorBlock, and a later middleware like SortBlocksByTypeAndKeyMiddleware deepcopies the library. Store name and reason and add __reduce__ so the exception round-trips. --- bibtexparser/middlewares/names.py | 8 ++++++++ tests/middleware_tests/test_names.py | 18 ++++++++++++++++++ 2 files changed, 26 insertions(+) diff --git a/bibtexparser/middlewares/names.py b/bibtexparser/middlewares/names.py index 45713a9..d784155 100644 --- a/bibtexparser/middlewares/names.py +++ b/bibtexparser/middlewares/names.py @@ -21,9 +21,17 @@ class InvalidNameError(ValueError): """Exception raised by :py:func:`parse_single_name_into_parts` when facing an invalid name.""" def __init__(self, name: str, reason: str): + self.name = name + self.reason = reason message: str = f"Cannot split the following name `{name}` into parts: {reason}" super().__init__(message) + def __reduce__(self): + # Rebuild from name and reason so the exception stays copyable and + # picklable; the base ValueError stores only the formatted message in + # args, which does not match this two-argument __init__. + return (self.__class__, (self.name, self.reason)) + class _NameTransformerMiddleware(BlockMiddleware, abc.ABC): """Internal utility class - superclass for all name-transforming middlewares. diff --git a/tests/middleware_tests/test_names.py b/tests/middleware_tests/test_names.py index e11133b..ed28ec2 100644 --- a/tests/middleware_tests/test_names.py +++ b/tests/middleware_tests/test_names.py @@ -132,6 +132,24 @@ def test_name_splitting_strict_mode(name: str, reason: str): parse_single_name_into_parts(name, strict=True) +def test_invalid_name_error_is_copyable(): + # A MiddlewareErrorBlock stores the InvalidNameError, and later middlewares + # (for example SortBlocksByTypeAndKeyMiddleware) deepcopy the library, so + # the exception has to survive copy and pickle. It could not before: its + # two-argument __init__ did not match the single message stored in args. + import pickle + + with pytest.raises(InvalidNameError) as exc_info: + parse_single_name_into_parts("AA, BB, CC, DD", strict=True) + error = exc_info.value + + for clone in (deepcopy(error), pickle.loads(pickle.dumps(error))): + assert isinstance(clone, InvalidNameError) + assert str(clone) == str(error) + assert clone.name == error.name + assert clone.reason == error.reason + + def _dict_to_nameparts(as_dict): return NameParts( first=as_dict["first"],