From 20c3289186114aebe4c61125eed323d45b661a40 Mon Sep 17 00:00:00 2001 From: Jonas Boos Date: Sun, 10 May 2026 20:17:32 +0000 Subject: [PATCH 1/3] MAINT: Replace try/except/pass with contextlib.suppress (SIM105) Replace 20 try/except/pass patterns with contextlib.suppress() as suggested by ruff's SIM105 rule. This is a readability improvement that simplifies error suppression in non-critical paths. Comments from the original except blocks have been preserved where they provided useful context (e.g., 'keep previous size', 'pragma: no cover', 'Occurs when called on PdfReader'). Also removes SIM105 from the ruff ignore list in pyproject.toml since all violations are now fixed. Contributes to #3327. Signed-off-by: Jonas Boos --- pypdf/_doc_common.py | 5 ++-- pypdf/_page.py | 19 ++++--------- pypdf/_reader.py | 10 +++---- pypdf/_text_extraction/_text_extractor.py | 5 ++-- pypdf/_writer.py | 34 +++++++---------------- pypdf/generic/_data_structures.py | 5 ++-- pypdf/generic/_viewerpref.py | 9 ++---- pyproject.toml | 1 - tests/test_reader.py | 5 ++-- 9 files changed, 31 insertions(+), 62 deletions(-) diff --git a/pypdf/_doc_common.py b/pypdf/_doc_common.py index aef7b41502..1610524ba0 100644 --- a/pypdf/_doc_common.py +++ b/pypdf/_doc_common.py @@ -28,6 +28,7 @@ # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE # POSSIBILITY OF SUCH DAMAGE. +import contextlib import struct from abc import abstractmethod from collections.abc import Generator, Iterable, Iterator, Mapping @@ -1045,10 +1046,8 @@ def _build_outline_item(self, node: DictionaryObject) -> Optional[Destination]: node.get("/Count", 0) >= 0 ) outline_item.node = node - try: + with contextlib.suppress(AttributeError): outline_item.indirect_reference = node.indirect_reference - except AttributeError: - pass return outline_item @property diff --git a/pypdf/_page.py b/pypdf/_page.py index 10171cd17b..9001684d18 100644 --- a/pypdf/_page.py +++ b/pypdf/_page.py @@ -27,6 +27,7 @@ # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE # POSSIBILITY OF SUCH DAMAGE. +import contextlib import math from collections.abc import Iterable, Iterator, Sequence from copy import deepcopy @@ -909,10 +910,8 @@ def compute_unique_key(base_key: str) -> tuple[str, bool]: if not same_value: if is_pdf_writer: new_res[newname] = page2res.raw_get(key).clone(pdf) - try: + with contextlib.suppress(AttributeError): new_res[newname] = new_res[newname].indirect_reference - except AttributeError: - pass else: new_res[newname] = page2res.raw_get(key) lst = sorted(new_res.items()) @@ -1021,11 +1020,9 @@ def replace_contents( if isinstance(self.get(PG.CONTENTS, None), ArrayObject): content_array = cast(ArrayObject, self[PG.CONTENTS]) for reference in content_array: - try: + # Occurs when called on PdfReader. + with contextlib.suppress(ValueError): writer._replace_object(indirect_reference=reference.indirect_reference, obj=NullObject()) - except ValueError: - # Occurs when called on PdfReader. - pass if isinstance(content, ArrayObject): content = ArrayObject(writer._add_object(obj) for obj in content) @@ -1272,10 +1269,8 @@ def _merge_page_writer( + trsf.apply_on((q[4], q[5]), True) + trsf.apply_on((q[6], q[7]), True) ) - try: + with contextlib.suppress(KeyError): aa["/Popup"][NameObject("/Parent")] = aa.indirect_reference - except KeyError: - pass try: aa[NameObject("/P")] = self.indirect_reference annots.append(aa.indirect_reference) @@ -1655,7 +1650,7 @@ def _debug_for_extract(self) -> str: # pragma: no cover out += enc_repr + "\n" except Exception: pass - try: + with contextlib.suppress(Exception): out += ( self[PG.RESOURCES]["/Font"][fo][ # type:ignore "/ToUnicode" @@ -1664,8 +1659,6 @@ def _debug_for_extract(self) -> str: # pragma: no cover .decode() + "\n" ) - except Exception: - pass except KeyError: out += "No Font\n" diff --git a/pypdf/_reader.py b/pypdf/_reader.py index db9279f405..8db42bc80a 100644 --- a/pypdf/_reader.py +++ b/pypdf/_reader.py @@ -48,6 +48,8 @@ else: from typing_extensions import Self +import contextlib + from ._doc_common import PdfDocCommon, convert_to_int from ._encryption import Encryption, PasswordType from ._utils import ( @@ -893,14 +895,10 @@ def _read_standard_xref_table(self, stream: StreamType) -> None: else: if entry_type_b == b"n": self.xref[generation][num] = offset - try: + with contextlib.suppress(Exception): self.xref_free_entry[generation][num] = entry_type_b == b"f" - except Exception: - pass - try: + with contextlib.suppress(Exception): self.xref_free_entry[65535][num] = entry_type_b == b"f" - except Exception: - pass cnt += 1 num += 1 read_non_whitespace(stream) diff --git a/pypdf/_text_extraction/_text_extractor.py b/pypdf/_text_extraction/_text_extractor.py index 866f1fdbf3..74eedbb507 100644 --- a/pypdf/_text_extraction/_text_extractor.py +++ b/pypdf/_text_extraction/_text_extractor.py @@ -27,6 +27,7 @@ # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE # POSSIBILITY OF SUCH DAMAGE. +import contextlib import math from typing import Any, Callable, Optional, Union @@ -300,10 +301,8 @@ def _handle_tf(self, operands: list[Any]) -> None: ) self._space_width = self.font.space_width / 2 # Actually the width of _half_ a space... - try: + with contextlib.suppress(Exception): # keep previous size self.font_size = float(operands[1]) - except Exception: - pass # keep previous size def _handle_td(self, operands: list[Any]) -> float: """Handle Td (Move text position) operation - Table 5.5 page 406.""" diff --git a/pypdf/_writer.py b/pypdf/_writer.py index 42c672711c..df4774f8fb 100644 --- a/pypdf/_writer.py +++ b/pypdf/_writer.py @@ -54,6 +54,8 @@ else: from typing_extensions import Self +import contextlib + from ._doc_common import DocumentInformation, PdfDocCommon from ._encryption import EncryptAlgorithm, Encryption from ._page import PageObject, Transformation @@ -362,10 +364,8 @@ def _info(self) -> Optional[DictionaryObject]: @_info.setter def _info(self, value: Optional[Union[IndirectObject, DictionaryObject]]) -> None: if value is None: - try: + with contextlib.suppress(KeyError, AttributeError): self._objects[self._info_obj.indirect_reference.idnum - 1] = None # type: ignore - except (KeyError, AttributeError): - pass self._info_obj = None else: if self._info_obj is None: @@ -518,12 +518,10 @@ def _add_page( # page; therefore in order to add multiple copies of the same # page, we need to create a new dictionary for the page, however the # objects below (including content) are not duplicated: - try: # delete an already existing page + with contextlib.suppress(Exception): # delete an already existing page del self._id_translated[id(page_org.indirect_reference.pdf)][ # type: ignore page_org.indirect_reference.idnum # type: ignore ] - except Exception: - pass page = cast( "PageObject", page_org.clone(self, False, excluded_keys).get_object() @@ -751,10 +749,8 @@ def open_destination( @open_destination.setter def open_destination(self, dest: Union[None, str, Destination, PageObject]) -> None: if dest is None: - try: + with contextlib.suppress(KeyError): del self._root_object["/OpenAction"] - except KeyError: - pass elif isinstance(dest, str): self._root_object[NameObject("/OpenAction")] = TextStringObject(dest) elif isinstance(dest, Destination): @@ -1255,10 +1251,8 @@ def clone_document_from_reader( ) # else: _info_obj = None done in clone_reader_document_root() - try: + with contextlib.suppress(AttributeError): self._ID = cast(ArrayObject, reader._ID).clone(self) - except AttributeError: - pass if callable(after_page_append): for page in cast( @@ -1704,10 +1698,8 @@ def replace_in_obj( if not is_null_or_none(self._info): unreferenced[self._info.indirect_reference.idnum - 1] = False # type: ignore - try: + with contextlib.suppress(AttributeError): unreferenced[self._ID.indirect_reference.idnum - 1] = False # type: ignore - except AttributeError: - pass for i in compress(range(len(self._objects)), unreferenced): self._objects[i] = None @@ -2156,10 +2148,8 @@ def _remove_objects_from_page__clean_forms( if k1 not in ["/Length", "/Filter", "/DecodeParms"] } ) - try: + with contextlib.suppress(AttributeError): # pragma: no cover content.indirect_reference = o.indirect_reference - except AttributeError: # pragma: no cover - pass stack.append(elt) # clean subforms @@ -3183,15 +3173,11 @@ def reset_translation( if reader is None: self._id_translated = {} elif isinstance(reader, PdfReader): - try: + with contextlib.suppress(Exception): del self._id_translated[id(reader)] - except Exception: - pass elif isinstance(reader, IndirectObject): - try: + with contextlib.suppress(Exception): del self._id_translated[id(reader.pdf)] - except Exception: - pass else: raise Exception("invalid parameter {reader}") diff --git a/pypdf/generic/_data_structures.py b/pypdf/generic/_data_structures.py index 12a0646532..02036fd396 100644 --- a/pypdf/generic/_data_structures.py +++ b/pypdf/generic/_data_structures.py @@ -29,6 +29,7 @@ __author__ = "Mathieu Fenniak" __author_email__ = "biziqe@mathieu.fenniak.net" +import contextlib import logging import re import sys @@ -1544,10 +1545,8 @@ def __init__(self, data: DictionaryObject) -> None: ) self.indirect_reference = data.indirect_reference for attr in field_attributes: - try: + with contextlib.suppress(KeyError): self[NameObject(attr)] = data[attr] - except KeyError: - pass if isinstance(self.get("/V"), EncodedStreamObject): d = cast(EncodedStreamObject, self[NameObject("/V")]).get_data() if isinstance(d, bytes): diff --git a/pypdf/generic/_viewerpref.py b/pypdf/generic/_viewerpref.py index 84fce613b1..fe57051690 100644 --- a/pypdf/generic/_viewerpref.py +++ b/pypdf/generic/_viewerpref.py @@ -26,6 +26,7 @@ # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE # POSSIBILITY OF SUCH DAMAGE. +import contextlib from typing import ( Any, Optional, @@ -43,10 +44,8 @@ def __init__(self, obj: Optional[DictionaryObject] = None) -> None: super().__init__(self) if not is_null_or_none(obj): self.update(obj.items()) # type: ignore - try: + with contextlib.suppress(AttributeError): self.indirect_reference = obj.indirect_reference # type: ignore - except AttributeError: - pass def _get_bool(self, key: str, default: Optional[BooleanObject]) -> Optional[BooleanObject]: return self.get(key, default) @@ -69,10 +68,8 @@ def _get_arr(self, key: str, default: Optional[list[Any]]) -> Optional[ArrayObje def _set_arr(self, key: str, v: Optional[ArrayObject]) -> None: if v is None: - try: + with contextlib.suppress(KeyError): del self[NameObject(key)] - except KeyError: - pass return if not isinstance(v, ArrayObject): raise ValueError("ArrayObject is expected") diff --git a/pyproject.toml b/pyproject.toml index d121340864..5559cf442b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -193,7 +193,6 @@ ignore = [ "RUF002", # Detect confusable Unicode-to-Unicode units. Introduces bugs "S101", # Use of `assert` detected "S110", # `try`-`except`-`pass` detected, consider logging the exception - "SIM105", # contextlib.suppress "SIM108", # Don't enforce ternary operators "SLF001", # Private member accessed "TC006", # To discuss: Add quotes to type expression in `typing.cast()` diff --git a/tests/test_reader.py b/tests/test_reader.py index b19be1b7dc..fe9521576b 100644 --- a/tests/test_reader.py +++ b/tests/test_reader.py @@ -1,4 +1,5 @@ """Test the pypdf._reader module.""" +import contextlib import io import struct import sys @@ -513,10 +514,8 @@ def test_read_malformed_header(caplog): PdfReader(io.BytesIO(b"foo"), strict=True) assert exc.value.args[0] == "PDF starts with 'foo', but '%PDF-' expected" caplog.clear() - try: + with contextlib.suppress(Exception): PdfReader(io.BytesIO(b"foo"), strict=False) - except Exception: - pass assert caplog.messages[0].startswith("invalid pdf header") From cfac9e520e36a387fb3137d3126383d0e3a316f7 Mon Sep 17 00:00:00 2001 From: Jonas Boos Date: Sun, 10 May 2026 20:27:59 +0000 Subject: [PATCH 2/3] MAINT: Fix import ordering for contextlib in _writer.py and _reader.py Move from after the conditional typing imports block to the stdlib imports section, following isort conventions. Signed-off-by: Jonas Boos --- pypdf/_reader.py | 3 +-- pypdf/_writer.py | 3 +-- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/pypdf/_reader.py b/pypdf/_reader.py index 8db42bc80a..83a09b96e5 100644 --- a/pypdf/_reader.py +++ b/pypdf/_reader.py @@ -27,6 +27,7 @@ # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE # POSSIBILITY OF SUCH DAMAGE. +import contextlib import os import re import sys @@ -48,8 +49,6 @@ else: from typing_extensions import Self -import contextlib - from ._doc_common import PdfDocCommon, convert_to_int from ._encryption import Encryption, PasswordType from ._utils import ( diff --git a/pypdf/_writer.py b/pypdf/_writer.py index df4774f8fb..7e0897d366 100644 --- a/pypdf/_writer.py +++ b/pypdf/_writer.py @@ -27,6 +27,7 @@ # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE # POSSIBILITY OF SUCH DAMAGE. +import contextlib import decimal import enum import hashlib @@ -54,8 +55,6 @@ else: from typing_extensions import Self -import contextlib - from ._doc_common import DocumentInformation, PdfDocCommon from ._encryption import EncryptAlgorithm, Encryption from ._page import PageObject, Transformation From 14ad76eb1fe8fedb2a8ff13d345fe313ea09eab1 Mon Sep 17 00:00:00 2001 From: Jonas Boos Date: Sun, 10 May 2026 20:36:14 +0000 Subject: [PATCH 3/3] MAINT: Trigger CI re-run Signed-off-by: Jonas Boos