From 6eab0c246f24f6a9bebb613d90a785918f331501 Mon Sep 17 00:00:00 2001 From: Claudia Pellegrino Date: Sun, 25 Oct 2026 21:02:30 +0100 Subject: [PATCH] [xmltodict] Fix regression in postprocessor MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Commit 15bc02e (PR #14695 on GitHub [1]) added a type signature for the `postprocessor` Callable but failed to take into account that upstream has a feature where that Callable may return `None`, and upstream interprets that as “throw away the node entirely in post-processing.” See [2] and [3] in the upstream code. Relax the return value for `postprocessor` so instead of: ```py tuple[str, _AttrValue] ``` it can return `None`: ```py tuple[str, _AttrValue] | None ``` That way, clients can now use that feature again, just like they used to in types-xmltodict 0.15.0.20250907 and earlier. [1]: https://github.com/python/typeshed/pull/14695 [2]: https://github.com/martinblech/xmltodict/blob/v1.0.4/xmltodict.py#L93-L97 [3]: https://github.com/martinblech/xmltodict/blob/v1.0.4/xmltodict.py#L160-L162 --- .../@tests/test_cases/check_postprocessor.py | 15 +++++++++++++++ stubs/xmltodict/xmltodict.pyi | 8 +++++--- 2 files changed, 20 insertions(+), 3 deletions(-) create mode 100644 stubs/xmltodict/@tests/test_cases/check_postprocessor.py diff --git a/stubs/xmltodict/@tests/test_cases/check_postprocessor.py b/stubs/xmltodict/@tests/test_cases/check_postprocessor.py new file mode 100644 index 000000000000..805a7aae7d14 --- /dev/null +++ b/stubs/xmltodict/@tests/test_cases/check_postprocessor.py @@ -0,0 +1,15 @@ +from __future__ import annotations + +import xmltodict +from xmltodict import _AttrDict, _AttrValue + + +# XML post-processor that eats all `b` elements +def b_eater(path: list[tuple[str, _AttrDict | None]], key: str, value: _AttrValue) -> tuple[str, _AttrValue] | None: + if key == "b": + return None # discard the node entirely + return key, value + + +xmltodict.parse("", postprocessor=b_eater) +# returns a tree of just diff --git a/stubs/xmltodict/xmltodict.pyi b/stubs/xmltodict/xmltodict.pyi index d0e1237d3360..50ba4b6f4fbe 100644 --- a/stubs/xmltodict/xmltodict.pyi +++ b/stubs/xmltodict/xmltodict.pyi @@ -20,7 +20,7 @@ class _DictSAXHandler: cdata_key: str force_cdata: bool | Container[str] | Callable[[tuple[str, _AttrDict | None], str, str], bool] cdata_separator: str - postprocessor: Callable[[list[tuple[str, _AttrDict | None]], str, _AttrValue], tuple[str, _AttrValue]] | None + postprocessor: Callable[[list[tuple[str, _AttrDict | None]], str, _AttrValue], tuple[str, _AttrValue] | None] | None dict_constructor: type strip_whitespace: bool namespace_separator: str @@ -37,7 +37,9 @@ class _DictSAXHandler: cdata_key: str = "#text", force_cdata: bool | Container[str] | Callable[[tuple[str, _AttrDict | None], str, str], bool] = False, cdata_separator: str = "", - postprocessor: Callable[[list[tuple[str, _AttrDict | None]], str, _AttrValue], tuple[str, _AttrValue]] | None = None, + postprocessor: ( + Callable[[list[tuple[str, _AttrDict | None]], str, _AttrValue], tuple[str, _AttrValue] | None] | None + ) = None, dict_constructor: type = ..., strip_whitespace: bool = True, namespace_separator: str = ":", @@ -68,7 +70,7 @@ def parse( cdata_key: str = "#text", force_cdata: bool | Container[str] | Callable[[tuple[str, _AttrDict | None], str, str], bool] = False, cdata_separator: str = "", - postprocessor: Callable[[list[tuple[str, _AttrDict | None]], str, _AttrValue], tuple[str, _AttrValue]] | None = None, + postprocessor: Callable[[list[tuple[str, _AttrDict | None]], str, _AttrValue], tuple[str, _AttrValue] | None] | None = None, dict_constructor: type = ..., strip_whitespace: bool = True, namespaces: Mapping[str, str | None] | None = None,