Script 'mail_helper' called by obssrc
Hello community,

here is the log from the commit of package python-netpbmfile for 
openSUSE:Factory checked in at 2026-02-17 18:14:24
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Comparing /work/SRC/openSUSE:Factory/python-netpbmfile (Old)
 and      /work/SRC/openSUSE:Factory/.python-netpbmfile.new.1977 (New)
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Package is "python-netpbmfile"

Tue Feb 17 18:14:24 2026 rev:7 rq:1333447 version:2026.1.29

Changes:
--------
--- /work/SRC/openSUSE:Factory/python-netpbmfile/python-netpbmfile.changes      
2025-05-30 17:26:18.137675425 +0200
+++ 
/work/SRC/openSUSE:Factory/.python-netpbmfile.new.1977/python-netpbmfile.changes
    2026-02-17 18:14:38.238158019 +0100
@@ -1,0 +2,8 @@
+Mon Feb 16 20:54:33 UTC 2026 - Dirk Müller <[email protected]>
+
+- update to 2026.1.29:
+  * Fix code review issues.
+  * Improve code quality.
+  * Drop support for Python 3.10, support Python 3.14.
+
+-------------------------------------------------------------------

Old:
----
  netpbmfile-2025.5.8.tar.gz

New:
----
  netpbmfile-2026.1.29.tar.gz

++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Other differences:
------------------
++++++ python-netpbmfile.spec ++++++
--- /var/tmp/diff_new_pack.m26e3T/_old  2026-02-17 18:14:39.142195686 +0100
+++ /var/tmp/diff_new_pack.m26e3T/_new  2026-02-17 18:14:39.150196020 +0100
@@ -1,7 +1,7 @@
 #
 # spec file for package python-netpbmfile
 #
-# Copyright (c) 2025 SUSE LLC
+# Copyright (c) 2026 SUSE LLC and contributors
 #
 # All modifications and additions to the file contributed by third parties
 # remain the property of their copyright owners, unless otherwise agreed
@@ -18,7 +18,7 @@
 
 %{?sle15_python_module_pythons}
 Name:           python-netpbmfile
-Version:        2025.5.8
+Version:        2026.1.29
 Release:        0
 Summary:        Read and write image files in the Netpbm format
 License:        BSD-3-Clause

++++++ netpbmfile-2025.5.8.tar.gz -> netpbmfile-2026.1.29.tar.gz ++++++
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/netpbmfile-2025.5.8/CHANGES.rst 
new/netpbmfile-2026.1.29/CHANGES.rst
--- old/netpbmfile-2025.5.8/CHANGES.rst 2025-05-09 06:44:40.000000000 +0200
+++ new/netpbmfile-2026.1.29/CHANGES.rst        2026-01-29 17:39:04.000000000 
+0100
@@ -1,6 +1,18 @@
 Revisions
 ---------
 
+2026.1.29
+
+- Fix code review issues.
+
+2026.1.8
+
+- Improve code quality.
+
+2025.12.12
+
+- Drop support for Python 3.10, support Python 3.14.
+
 2025.5.8
 
 - Remove doctest command line option.
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/netpbmfile-2025.5.8/LICENSE 
new/netpbmfile-2026.1.29/LICENSE
--- old/netpbmfile-2025.5.8/LICENSE     2025-05-09 06:44:40.000000000 +0200
+++ new/netpbmfile-2026.1.29/LICENSE    2026-01-29 17:39:04.000000000 +0100
@@ -1,6 +1,6 @@
 BSD-3-Clause license
 
-Copyright (c) 2011-2025, Christoph Gohlke
+Copyright (c) 2011-2026, Christoph Gohlke
 All rights reserved.
 
 Redistribution and use in source and binary forms, with or without
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/netpbmfile-2025.5.8/MANIFEST.in 
new/netpbmfile-2026.1.29/MANIFEST.in
--- old/netpbmfile-2025.5.8/MANIFEST.in 2025-05-09 06:44:40.000000000 +0200
+++ new/netpbmfile-2026.1.29/MANIFEST.in        2026-01-29 17:39:04.000000000 
+0100
@@ -5,7 +5,11 @@
 
 include netpbmfile/py.typed
 
+exclude .env
 exclude *.cmd
+exclude *.yaml
+exclude mypy.ini
+exclude ruff.toml
 recursive-exclude doc *
 recursive-exclude docs *
 recursive-exclude test *
@@ -21,4 +25,4 @@
 
 # include docs/conf.py
 # include docs/make.py
-# include docs/_static/custom.css
\ No newline at end of file
+# include docs/_static/custom.css
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/netpbmfile-2025.5.8/README.rst 
new/netpbmfile-2026.1.29/README.rst
--- old/netpbmfile-2025.5.8/README.rst  2025-05-09 06:44:40.000000000 +0200
+++ new/netpbmfile-2026.1.29/README.rst 2026-01-29 17:39:04.000000000 +0100
@@ -11,7 +11,7 @@
 - PGM (Portable Gray Map): P2 (text) and P5 (binary)
 - PPM (Portable Pixel Map): P3 (text) and P6 (binary)
 - PNM (Portable Any Map): shorthand for PBM, PGM, and PPM collectively
-- PAM (Portable Arbitrary Map): P7, bilevel, gray, and rgb
+- PAM (Portable Arbitrary Map): P7, bilevel, gray, rgb, and arbitrary depths
 - PGX (Portable Graymap Signed): PG, signed grayscale
 - PFM (Portable Float Map): Pf (gray), PF (rgb), and PF4 (rgba), read-only
 - XV thumbnail: P7 332 (rgb332), read-only
@@ -24,7 +24,8 @@
 
 :Author: `Christoph Gohlke <https://www.cgohlke.com>`_
 :License: BSD-3-Clause
-:Version: 2025.5.8
+:Version: 2026.1.29
+:DOI: `10.5281/zenodo.17903402 <https://doi.org/10.5281/zenodo.17903402>`_
 
 Quickstart
 ----------
@@ -44,55 +45,34 @@
 This revision was tested with the following requirements and dependencies
 (other versions may work):
 
-- `CPython <https://www.python.org>`_ 3.10.11, 3.11.9, 3.12.9, 3.13.2 64-bit
-- `NumPy <https://pypi.org/project/numpy/>`_ 2.2.5
+- `CPython <https://www.python.org>`_ 3.11.9, 3.12.10, 3.13.11, 3.14.2 64-bit
+- `NumPy <https://pypi.org/project/numpy>`_ 2.4.1
 
 Revisions
 ---------
 
-2025.5.8
-
-- Remove doctest command line option.
-
-2025.1.1
+2026.1.29
 
-- Improve type hints.
-- Drop support for Python 3.9, support Python 3.13.
+- Fix code review issues.
 
-2024.5.24
+2026.1.8
 
-- Fix docstring examples not correctly rendered on GitHub.
+- Improve code quality.
 
-2024.4.24
+2025.12.12
 
-- Support NumPy 2.
+- Drop support for Python 3.10, support Python 3.14.
 
-2023.8.30
+2025.5.8
 
-- Fix linting issues.
-- Add py.typed marker.
+- Remove doctest command line option.
 
-2023.6.15
+2025.1.1
 
-- Drop support for Python 3.8 and numpy < 1.21 (NEP29).
 - Improve type hints.
+- Drop support for Python 3.9, support Python 3.13.
 
-2023.1.1
-
-- Several breaking changes:
-- Rename magicnum to magicnumber (breaking).
-- Rename tupltypes to tupltype (breaking).
-- Change magicnumber and header properties to str (breaking).
-- Replace pam parameter with magicnumber (breaking).
-- Move byteorder parameter from NetpbmFile.asarray to NetpbmFile (breaking).
-- Fix shape and axes properties for multi-image files.
-- Add maxval and tupltype parameters to NetpbmFile.fromdata and imwrite.
-- Add option to write comment to PNM and PAM files.
-- Support writing PGX and text formats.
-- Add Google style docstrings.
-- Add unittests.
-
-2022.10.25
+2024.5.24
 
 - …
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/netpbmfile-2025.5.8/netpbmfile/__init__.py 
new/netpbmfile-2026.1.29/netpbmfile/__init__.py
--- old/netpbmfile-2025.5.8/netpbmfile/__init__.py      2025-05-09 
06:44:40.000000000 +0200
+++ new/netpbmfile-2026.1.29/netpbmfile/__init__.py     2026-01-29 
17:39:04.000000000 +0100
@@ -3,15 +3,7 @@
 from .netpbmfile import *
 from .netpbmfile import __all__, __doc__, __version__
 
+# constants are repeated for documentation
 
-def _set_module() -> None:
-    """Set __module__ attribute for all public objects."""
-    globs = globals()
-    module = globs['__name__']
-    for item in __all__:
-        obj = globs[item]
-        if hasattr(obj, '__module__'):
-            obj.__module__ = module
-
-
-_set_module()
+__version__ = __version__
+"""Netpbmfile version string."""
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/netpbmfile-2025.5.8/netpbmfile/netpbmfile.py 
new/netpbmfile-2026.1.29/netpbmfile/netpbmfile.py
--- old/netpbmfile-2025.5.8/netpbmfile/netpbmfile.py    2025-05-09 
06:44:40.000000000 +0200
+++ new/netpbmfile-2026.1.29/netpbmfile/netpbmfile.py   2026-01-29 
17:39:04.000000000 +0100
@@ -1,6 +1,6 @@
 # netpbmfile.py
 
-# Copyright (c) 2011-2025, Christoph Gohlke
+# Copyright (c) 2011-2026, Christoph Gohlke
 # All rights reserved.
 #
 # Redistribution and use in source and binary forms, with or without
@@ -38,7 +38,7 @@
 - PGM (Portable Gray Map): P2 (text) and P5 (binary)
 - PPM (Portable Pixel Map): P3 (text) and P6 (binary)
 - PNM (Portable Any Map): shorthand for PBM, PGM, and PPM collectively
-- PAM (Portable Arbitrary Map): P7, bilevel, gray, and rgb
+- PAM (Portable Arbitrary Map): P7, bilevel, gray, rgb, and arbitrary depths
 - PGX (Portable Graymap Signed): PG, signed grayscale
 - PFM (Portable Float Map): Pf (gray), PF (rgb), and PF4 (rgba), read-only
 - XV thumbnail: P7 332 (rgb332), read-only
@@ -51,7 +51,8 @@
 
 :Author: `Christoph Gohlke <https://www.cgohlke.com>`_
 :License: BSD-3-Clause
-:Version: 2025.5.8
+:Version: 2026.1.29
+:DOI: `10.5281/zenodo.17903402 <https://doi.org/10.5281/zenodo.17903402>`_
 
 Quickstart
 ----------
@@ -71,55 +72,34 @@
 This revision was tested with the following requirements and dependencies
 (other versions may work):
 
-- `CPython <https://www.python.org>`_ 3.10.11, 3.11.9, 3.12.9, 3.13.2 64-bit
-- `NumPy <https://pypi.org/project/numpy/>`_ 2.2.5
+- `CPython <https://www.python.org>`_ 3.11.9, 3.12.10, 3.13.11, 3.14.2 64-bit
+- `NumPy <https://pypi.org/project/numpy>`_ 2.4.1
 
 Revisions
 ---------
 
-2025.5.8
+2026.1.29
 
-- Remove doctest command line option.
+- Fix code review issues.
 
-2025.1.1
+2026.1.8
 
-- Improve type hints.
-- Drop support for Python 3.9, support Python 3.13.
+- Improve code quality.
 
-2024.5.24
-
-- Fix docstring examples not correctly rendered on GitHub.
+2025.12.12
 
-2024.4.24
+- Drop support for Python 3.10, support Python 3.14.
 
-- Support NumPy 2.
-
-2023.8.30
+2025.5.8
 
-- Fix linting issues.
-- Add py.typed marker.
+- Remove doctest command line option.
 
-2023.6.15
+2025.1.1
 
-- Drop support for Python 3.8 and numpy < 1.21 (NEP29).
 - Improve type hints.
+- Drop support for Python 3.9, support Python 3.13.
 
-2023.1.1
-
-- Several breaking changes:
-- Rename magicnum to magicnumber (breaking).
-- Rename tupltypes to tupltype (breaking).
-- Change magicnumber and header properties to str (breaking).
-- Replace pam parameter with magicnumber (breaking).
-- Move byteorder parameter from NetpbmFile.asarray to NetpbmFile (breaking).
-- Fix shape and axes properties for multi-image files.
-- Add maxval and tupltype parameters to NetpbmFile.fromdata and imwrite.
-- Add option to write comment to PNM and PAM files.
-- Support writing PGX and text formats.
-- Add Google style docstrings.
-- Add unittests.
-
-2022.10.25
+2024.5.24
 
 - …
 
@@ -164,9 +144,9 @@
 
 from __future__ import annotations
 
-__version__ = '2025.5.8'
+__version__ = '2026.1.29'
 
-__all__ = ['__version__', 'imread', 'imwrite', 'imsave', 'NetpbmFile']
+__all__ = ['NetpbmFile', '__version__', 'imread', 'imsave', 'imwrite']
 
 import logging
 import math
@@ -180,7 +160,8 @@
 
 if TYPE_CHECKING:
     from collections.abc import Iterable
-    from typing import Any, BinaryIO, Literal
+    from types import TracebackType
+    from typing import Any, BinaryIO, ClassVar, Literal, Self
 
     from numpy.typing import ArrayLike, NDArray
 
@@ -219,8 +200,7 @@
 
     """
     with NetpbmFile(file, byteorder=byteorder) as netpbm:
-        image = netpbm.asarray()
-    return image
+        return netpbm.asarray()
 
 
 def imwrite(
@@ -279,9 +259,10 @@
     Parameters:
         file:
             Name of file or open binary file to read.
+            None creates an empty instance.
         byteorder:
             Byte order of image data in file.
-            By default, all formats are big endian except for PFM, which
+            By default, all formats are big-endian except for PFM, which
             encodes the byte order in the file header.
 
     """
@@ -302,13 +283,13 @@
     """Number of columns in image."""
 
     depth: int
-    """Number of samples in image."""
+    """Number of samples per pixel."""
 
     maxval: int
     """Maximum value of image samples."""
 
     scale: float
-    """Factor to scale image values in PFM formats."""
+    """Factor to scale image values in PFM formats, else zero."""
 
     byteorder: ByteOrder
     """Byte order of binary image data."""
@@ -328,7 +309,7 @@
     _data: NDArray[Any] | None
     _fh: BinaryIO | None
 
-    MAGIC_NUMBER: dict[str, str] = {
+    MAGIC_NUMBER: ClassVar[dict[str, str]] = {
         'P1': 'BLACKANDWHITE',
         'P2': 'GRAYSCALE',
         'P3': 'RGB',
@@ -371,20 +352,24 @@
             return
 
         if isinstance(file, (str, os.PathLike)):
-            self._fh = open(file, 'rb')
+            self._fh = open(file, 'rb')  # noqa: SIM115
             self.filename = os.fspath(file)
         else:
             self._fh = file
 
+        self._fh.seek(0)
+        data = self._fh.read(4096)
+        if (
+            len(data) < 7
+            or not data[:2].isascii()
+            or data[:2].decode('ascii') not in NetpbmFile.MAGIC_NUMBER
+        ):
+            if self.filename:
+                self._fh.close()
+            msg = f'not a Netpbm file:\n  {data[:16]!r}'
+            raise ValueError(msg)
+
         try:
-            self._fh.seek(0)
-            data = self._fh.read(4096)
-            if (
-                len(data) < 7
-                or not data[:2].isascii()
-                or data[:2].decode('ascii') not in NetpbmFile.MAGIC_NUMBER
-            ):
-                raise ValueError(f'not a Netpbm file:\n  {data[:16]!r}')
             if data[:2] in b'PFPf':
                 self._read_pf_header(data)
             elif data[:2] == b'PG':
@@ -396,9 +381,8 @@
                     try:
                         self._read_pnm_header(data)
                     except Exception as exc:
-                        raise ValueError(
-                            f'not a Netpbm file:\n{data[:16]!r}'
-                        ) from exc
+                        msg = f'not a Netpbm file:\n{data[:16]!r}'
+                        raise ValueError(msg) from exc
         except Exception:
             self._fh.close()
             raise
@@ -419,7 +403,8 @@
         elif self.maxval < 2**32:
             dtype = self.byteorder + 'u4'
         else:
-            raise ValueError(f'{self.maxval=} out of range')
+            msg = f'{self.maxval=} out of range'
+            raise ValueError(msg)
 
         self.dtype = numpy.dtype(dtype)
 
@@ -430,14 +415,15 @@
             shape = [
                 self.height,
                 (
-                    int(math.ceil(self.width / 8))
+                    math.ceil(self.width / 8)
                     if self.magicnumber in {'P4'}
                     else self.width
                 ),
                 self.depth,
                 self.dtype.itemsize,
             ]
-            self.frames = max(1, bytecount // product(shape))
+            prod = product(shape)
+            self.frames = max(1, bytecount // prod) if prod > 0 else 1
 
     @classmethod
     def fromdata(
@@ -470,23 +456,25 @@
         data = numpy.array(data, ndmin=2, copy=True)
         if data.dtype.kind not in 'uib':
             # TODO: support PF, Pf, PF4
-            raise ValueError(f'dtype {data.dtype!r} not supported')
+            msg = f'dtype {data.dtype!r} not supported'
+            raise ValueError(msg)
 
         issigned = data.dtype.kind == 'i' and numpy.min(data) < 0
         if issigned:
             if magicnumber is None:
                 magicnumber = 'PG'
             elif magicnumber != 'PG':
-                raise ValueError(
-                    f'invalid {data.dtype=!r} for {magicnumber=!r}'
-                )
+                msg = f'invalid {data.dtype=!r} for {magicnumber=!r}'
+                raise ValueError(msg)
 
         if maxval is None:
             if issigned:
                 maxval = int(numpy.max(numpy.abs(data)))
             else:
                 maxval = int(numpy.max(data))
-            if maxval == 1:
+            if maxval == 0 and not issigned:
+                maxval = 1  # treat all-zero unsigned arrays as binary
+            elif maxval == 1:
                 maxval = 1
             else:
                 maxval = max(
@@ -494,7 +482,8 @@
                 )
         if not 0 < maxval < 2**32:
             # allow maxval > 65535
-            raise ValueError(f'{maxval=} of range')
+            msg = f'{maxval=} out of range'
+            raise ValueError(msg)
 
         self = cls(None)
 
@@ -524,21 +513,24 @@
         elif magicnumber in {'P3', 'P6'}:
             # rgb
             if data.ndim < 3 or data.shape[-1] != 3:
-                raise ValueError(f'invalid {magicnumber=!r} for {data.shape=}')
+                msg = f'invalid {magicnumber=!r} for {data.shape=}'
+                raise ValueError(msg)
             self.depth = data.shape[-1]
             self.width = data.shape[-2]
             self.height = data.shape[-3]
         elif magicnumber in {'P1', 'P2', 'P4', 'P5', 'PG'}:
             # bilevel or gray
             if magicnumber in {'P1', 'P4'} and maxval != 1:
-                raise ValueError(f'invalid {magicnumber=!r} for {maxval=}')
+                msg = f'invalid {magicnumber=!r} for {maxval=}'
+                raise ValueError(msg)
             if magicnumber == 'PG':
-                cls.byteorder = '<' if data.dtype.byteorder in '<|=' else '>'
+                self.byteorder = '<' if data.dtype.byteorder in '<=' else '>'
             self.depth = 1
             self.width = data.shape[-1]
             self.height = data.shape[-2]
         else:
-            raise ValueError(f'invalid {magicnumber=}')
+            msg = f'invalid {magicnumber=}'
+            raise ValueError(msg)
 
         if magicnumber == 'PG' and data.dtype.kind == 'i':
             self._data = data.astype(
@@ -546,9 +538,9 @@
                     'i1'
                     if maxval < 128
                     else (
-                        cls.byteorder + 'i2'
+                        self.byteorder + 'i2'
                         if maxval < 32768
-                        else cls.byteorder + 'i4'
+                        else self.byteorder + 'i4'
                     )
                 ),
                 copy=False,
@@ -561,17 +553,16 @@
                 copy=False,
             )
 
-        self.frames = max(
-            1, product(data.shape) // (self.height * self.width * self.depth)
+        frame_size = self.height * self.width * self.depth
+        self.frames = (
+            max(1, product(data.shape) // frame_size) if frame_size > 0 else 1
         )
-        assert magicnumber is not None
         self.magicnumber = magicnumber
         self.maxval = maxval
         self.dtype = self._data.dtype
         self.header = self._header()
 
         if tupltype is not None:
-            assert tupltype is not None
             self.tupltype = tupltype
         elif magicnumber != 'P7':
             self.tupltype = NetpbmFile.MAGIC_NUMBER[self.magicnumber]
@@ -650,7 +641,7 @@
                     comment=comment,
                 )
         else:
-            assert hasattr(file, 'seek')
+            # assert hasattr(file, 'seek')
             self._tofile(
                 file,
                 magicnumber=magicnumber,
@@ -671,7 +662,7 @@
         if self.depth > 1:
             shape += [self.depth]
         if self.frames > 1:
-            shape = [self.frames] + shape
+            shape = [self.frames, *shape]
         return tuple(shape)
 
     @property
@@ -693,12 +684,15 @@
             data,
         )
         if match is None:
-            raise ValueError('invalid PAM header')
+            msg = 'invalid PAM header'
+            raise ValueError(msg)
         regroups = match.groups()
         self.dataoffset = len(regroups[0])
         self.header = regroups[0].decode(errors='ignore')
         self.magicnumber = 'P7'
         for group in regroups[1:]:
+            if group is None:
+                continue
             key, value = group.split()
             setattr(self, key.decode('ascii').lower(), int(value))
         matches = re.findall(r'(TUPLTYPE\s+\w+)', self.header)
@@ -723,7 +717,8 @@
             data,
         )
         if match is None:
-            raise ValueError('invalid PNM header')
+            msg = 'invalid PNM header'
+            raise ValueError(msg)
         regroups = match.groups()
         regroups = regroups + (1,) * bpm
         self.dataoffset = len(regroups[0])
@@ -732,14 +727,12 @@
         self.width = int(regroups[2])
         self.height = int(regroups[3])
         self.maxval = int(regroups[4])
-        self.depth = (
-            3 if self.magicnumber in {'P3', 'P6', 'P7', 'P7 332'} else 1
-        )
+        self.depth = 3 if self.magicnumber in {'P3', 'P6', 'P7 332'} else 1
         self.tupltype = NetpbmFile.MAGIC_NUMBER[self.magicnumber]
 
     def _read_pf_header(self, data: bytes, /) -> None:
         """Read PF header and initialize instance."""
-        # there are no comments in these files
+        # there are no comments in these files, but allow anyway
         match = re.search(
             br'(^(PF|PF4|Pf)\s+(?:#.*[\r\n])*'
             br'\s*(\d+)\s+(?:#.*[\r\n])*'
@@ -749,7 +742,8 @@
             data,
         )
         if match is None:
-            raise ValueError('invalid PF header')
+            msg = 'invalid PF header'
+            raise ValueError(msg)
         regroups = match.groups()
         self.dataoffset = len(regroups[0])
         self.header = regroups[0].decode(errors='ignore')
@@ -765,7 +759,8 @@
         elif self.magicnumber == 'Pf':
             self.depth = 1
         else:
-            raise ValueError(f'invalid {self.magicnumber=!r}')
+            msg = f'invalid {self.magicnumber=!r}'
+            raise ValueError(msg)
 
     def _read_pg_header(self, data: bytes, /) -> None:
         """Read PG header and initialize instance."""
@@ -774,11 +769,12 @@
             br'(LM|ML)?[ ]*'
             br'([-+])?[ ]*([0-9]+)[ ]+'
             br'([0-9]+)[ ]+'
-            br'([0-9]+)[ ]*[\r?\n])',
+            br'([0-9]+)[ ]*[\r\n])',
             data,
         )
         if match is None:
-            raise ValueError('invalid PG header')
+            msg = 'invalid PG header'
+            raise ValueError(msg)
         regroups = match.groups()
         self.dataoffset = len(regroups[0])
         self.header = regroups[0].decode(errors='ignore')
@@ -801,7 +797,8 @@
                 self.byteorder + ('i4' if signed else 'u4')
             )
         else:
-            raise ValueError(f'{bitdepth=} out of range')
+            msg = f'{bitdepth=} out of range'
+            raise ValueError(msg)
 
     def _read_data(self, fh: BinaryIO) -> NDArray[Any]:
         """Return image data from open file."""
@@ -814,7 +811,8 @@
         rawdata = fh.read()
 
         if self.magicnumber in {'P1', 'P2', 'P3'}:
-            if bilevel and rawdata.strip()[1:2] in b'01':
+            stripped = rawdata.strip()
+            if bilevel and stripped[1:2] and stripped[1:2] in b'01':
                 datalist = [
                     bytes([i])
                     for line in rawdata.splitlines()
@@ -833,7 +831,7 @@
             data = numpy.array(datalist[:size], dtype).reshape(shape)
         else:
             if bilevel:
-                shape[2] = int(math.ceil(self.width / 8))
+                shape[2] = math.ceil(self.width / 8)
             size = product(shape[1:]) * dtype.itemsize
             size *= max(1, len(rawdata) // size)
             data = numpy.frombuffer(rawdata[:size], dtype).reshape(shape)
@@ -866,7 +864,8 @@
         if magicnumber is None:
             magicnumber = self.magicnumber
         if magicnumber not in NetpbmFile.MAGIC_NUMBER:
-            raise ValueError(f'invalid {magicnumber=!r}')
+            msg = f'invalid {magicnumber=!r}'
+            raise ValueError(msg)
 
         fh.seek(0)
         fh.write(
@@ -875,10 +874,7 @@
             )
         )
 
-        if self._data is None:
-            data = self.asarray(copy=False)
-        else:
-            data = self._data
+        data = self.asarray(copy=False) if self._data is None else self._data
 
         # data type/shape verification done in fromdata() and _header()
         if magicnumber == 'P1':
@@ -891,7 +887,7 @@
             numpy.savetxt(fh, data.reshape(-1), fmt='%i')
         elif magicnumber == 'P2':
             if self.maxval > 65535:
-                logger().warning('writing non-compliant maxval {self.maxval}')
+                logger().warning(f'writing non-compliant maxval {self.maxval}')
             if self.frames > 1:
                 logger().warning('writing non-compliant multi-image file')
             assert self.depth == 1
@@ -900,7 +896,7 @@
             numpy.savetxt(fh, data.reshape(-1), fmt='%i')
         elif magicnumber == 'P3':
             if self.maxval > 65535:
-                logger().warning('writing non-compliant maxval {self.maxval}')
+                logger().warning(f'writing non-compliant maxval {self.maxval}')
             if self.frames > 1:
                 logger().warning('writing non-compliant multi-image file')
             assert self.depth == 3
@@ -939,16 +935,17 @@
         if comment is None:
             comment = ''  # f'written by netpbmfile {__version__}'
         if comment:
-            comment = comment.split('\n')[0].strip().encode('ascii').decode()
-        if comment:
-            comment = f'\n# {comment[:66]}\n'
-        else:
-            comment = ' '
+            comment = (
+                comment.split('\n')[0]
+                .strip()
+                .encode('ascii', errors='replace')
+                .decode()
+            )
+        comment = f'\n# {comment[:66]}\n' if comment else ' '
         if magicnumber.startswith('P7'):
             if self.maxval < 1 or self.dtype.kind not in 'bu':
-                raise ValueError(
-                    f'data not compatible with {magicnumber!r} format'
-                )
+                msg = f'data not compatible with {magicnumber!r} format'
+                raise ValueError(msg)
             return '\n'.join(
                 (
                     f'P7{comment[:-1]}',
@@ -964,51 +961,52 @@
             )
         if magicnumber == 'PG':
             if self.dtype.kind not in 'iu':
-                raise ValueError(
-                    f'data not compatible with {magicnumber!r} format'
-                )
-            bitdepth = int(math.ceil(math.log2(self.maxval + 1)))
+                msg = f'data not compatible with {magicnumber!r} format'
+                raise ValueError(msg)
+            bitdepth = math.ceil(math.log2(self.maxval + 1))
             if self.dtype.kind == 'i':
                 bitdepth += 1
             return ''.join(
                 (
                     'PG ',  # do not allow comments
-                    'ML ' if self.byteorder == '>' else 'LM',
+                    'ML ' if self.byteorder == '>' else 'LM ',
                     '-' if self.dtype.kind == 'i' else '',
-                    f'{bitdepth} ',
-                    f'{self.width} ' f'{self.height}\n',
+                    f'{bitdepth} {self.width} {self.height}\n',
                 )
             )
         if magicnumber in {'P1', 'P4'}:
             if self.maxval != 1 or self.depth != 1 or self.dtype.kind != 'b':
-                raise ValueError(
-                    f'data not compatible with {magicnumber!r} format'
-                )
+                msg = f'data not compatible with {magicnumber!r} format'
+                raise ValueError(msg)
             return f'{magicnumber}{comment}{self.width} {self.height}\n'
         if magicnumber in {'P2', 'P5'}:
             if self.depth != 1 or self.dtype.kind not in 'ui':
-                raise ValueError(
-                    f'data not compatible with {magicnumber!r} format'
-                )
+                msg = f'data not compatible with {magicnumber!r} format'
+                raise ValueError(msg)
             return (
                 f'{magicnumber}{comment}'
                 f'{self.width} {self.height} {self.maxval}\n'
             )
         if magicnumber in {'P3', 'P6'}:
             if self.depth != 3 or self.dtype.kind not in 'ui':
-                raise ValueError(
-                    f'data not compatible with {magicnumber!r} format'
-                )
+                msg = f'data not compatible with {magicnumber!r} format'
+                raise ValueError(msg)
             return (
                 f'{magicnumber}{comment}'
                 f'{self.width} {self.height} {self.maxval}\n'
             )
-        raise ValueError(f'writing {magicnumber!r} format not supported')
+        msg = f'writing {magicnumber!r} format not supported'
+        raise ValueError(msg)
 
-    def __enter__(self) -> NetpbmFile:
+    def __enter__(self) -> Self:
         return self
 
-    def __exit__(self, exc_type: Any, exc_value: Any, traceback: Any) -> None:
+    def __exit__(
+        self,
+        exc_type: type[BaseException] | None,
+        exc_value: BaseException | None,
+        traceback: TracebackType | None,
+    ) -> None:
         self.close()
 
     def __repr__(self) -> str:
@@ -1037,10 +1035,14 @@
 
 
 def product(iterable: Iterable[int], /) -> int:
-    """Return product of sequence of numbers."""
+    """Return product of integers.
+
+    Like math.prod, but does not overflow with numpy arrays.
+
+    """
     prod = 1
     for i in iterable:
-        prod *= i
+        prod *= int(i)
     return prod
 
 
@@ -1060,7 +1062,7 @@
 def main(argv: list[str] | None = None) -> int:
     """Command line usage main function.
 
-    Show images specified on command line or all images in  directory.
+    Show images specified on command line or all images in directory.
 
     """
     from glob import glob
@@ -1085,40 +1087,38 @@
     else:
         files = argv[1:]
 
-    for fname in files:
+    for filename in files:
         try:
-            with NetpbmFile(fname) as pam:
-                print(pam)
+            with NetpbmFile(filename) as pam:
+                print(pam, '\n')  # noqa: T201
                 img = pam.asarray(copy=False)
-                print()
         except ValueError as exc:
             # raise  # enable for debugging
-            print(fname, exc)
+            print(filename, exc)  # noqa: T201
             continue
 
         cmap = 'binary' if pam.maxval == 1 else 'gray'
         dtype = img.dtype
         shape = img.shape
-        title = f'{os.path.split(fname)[-1]} {pam.magicnumber} {shape} {dtype}'
+        title = (
+            f'{os.path.split(filename)[-1]} {pam.magicnumber} {shape} {dtype}'
+        )
 
         multiimage = img.ndim > 3 or (
             img.ndim > 2 and img.shape[-1] not in {3, 4}
         )
         if tifffile is None or not multiimage:
             if img.ndim > 3 or (img.ndim > 2 and img.shape[-1] not in {3, 4}):
-                warnings.warn('displaying first image only')
+                warnings.warn('displaying first image only', stacklevel=2)
                 img = img[0]
             if img.shape[-1] in {3, 4} and pam.maxval != 255:
-                warnings.warn('converting RGB image for display')
+                warnings.warn('converting RGB image for display', stacklevel=2)
                 maxval = float(
                     numpy.max(img)
                     if pam.maxval is None  # type: ignore[redundant-expr]
                     else pam.maxval
                 )
-                if maxval > 0.0:
-                    img = img / maxval
-                else:
-                    img = img.copy()
+                img = img / maxval if maxval > 0.0 else img.copy()
                 img *= 255
                 numpy.rint(img, out=img)
                 numpy.clip(img, 0, 255, out=img)
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/netpbmfile-2025.5.8/pyproject.toml 
new/netpbmfile-2026.1.29/pyproject.toml
--- old/netpbmfile-2025.5.8/pyproject.toml      2025-05-09 06:44:40.000000000 
+0200
+++ new/netpbmfile-2026.1.29/pyproject.toml     2026-01-29 17:39:04.000000000 
+0100
@@ -1,3 +1,13 @@
 [build-system]
 requires = ["setuptools"]
 build-backend = "setuptools.build_meta"
+
+[tool.black]
+line-length = 79
+target-version = ["py311", "py312", "py313", "py314"]
+skip-string-normalization = true
+
+[tool.isort]
+known_first_party = ["netpbmfile"]
+profile = "black"
+line_length = 79
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/netpbmfile-2025.5.8/setup.py 
new/netpbmfile-2026.1.29/setup.py
--- old/netpbmfile-2025.5.8/setup.py    2025-05-09 06:44:40.000000000 +0200
+++ new/netpbmfile-2026.1.29/setup.py   2026-01-29 17:39:04.000000000 +0100
@@ -14,7 +14,8 @@
     """Return first match of pattern in string."""
     match = re.search(pattern, string, flags)
     if match is None:
-        raise ValueError(f'{pattern!r} not found')
+        msg = f'{pattern=!r} not found'
+        raise ValueError(msg)
     return match.groups()[0]
 
 
@@ -50,7 +51,7 @@
     re.MULTILINE | re.DOTALL,
 )
 readme = '\n'.join(
-    [description, '=' * len(description)] + readme.splitlines()[1:]
+    [description, '=' * len(description), *readme.splitlines()[1:]]
 )
 
 if 'sdist' in sys.argv:
@@ -106,7 +107,7 @@
     entry_points={
         'console_scripts': ['netpbmfile = netpbmfile.netpbmfile:main']
     },
-    python_requires='>=3.10',
+    python_requires='>=3.11',
     install_requires=['numpy'],
     extras_require={'all': ['tifffile', 'matplotlib']},
     platforms=['any'],
@@ -116,9 +117,9 @@
         'Intended Audience :: Developers',
         'Operating System :: OS Independent',
         'Programming Language :: Python :: 3 :: Only',
-        'Programming Language :: Python :: 3.10',
         'Programming Language :: Python :: 3.11',
         'Programming Language :: Python :: 3.12',
         'Programming Language :: Python :: 3.13',
+        'Programming Language :: Python :: 3.14',
     ],
 )
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/netpbmfile-2025.5.8/tests/conftest.py 
new/netpbmfile-2026.1.29/tests/conftest.py
--- old/netpbmfile-2025.5.8/tests/conftest.py   2025-05-09 06:44:40.000000000 
+0200
+++ new/netpbmfile-2026.1.29/tests/conftest.py  2026-01-29 17:39:04.000000000 
+0100
@@ -1,5 +1,7 @@
 # netpbmfile/tests/conftest.py
 
+"""Pytest configuration."""
+
 import os
 import sys
 
@@ -10,15 +12,15 @@
     )
 
 
-def pytest_report_header(config):
+def pytest_report_header(config: object) -> str:
+    """Return pytest report header."""
     try:
-        pyversion = f'Python {sys.version.splitlines()[0]}'
         import netpbmfile
 
-        return '{}\npackagedir: {}\nversion: netpbmfile {}'.format(
-            pyversion,
-            netpbmfile.__path__[0],
-            netpbmfile.__version__,
+        return (
+            f'Python {sys.version.splitlines()[0]}\n'
+            f'packagedir: {netpbmfile.__path__[0]}\n'
+            f'version: netpbmfile {netpbmfile.__version__}'
         )
     except Exception as exc:
         return f'pytest_report_header failed: {exc!s}'
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/netpbmfile-2025.5.8/tests/test_netpbmfile.py 
new/netpbmfile-2026.1.29/tests/test_netpbmfile.py
--- old/netpbmfile-2025.5.8/tests/test_netpbmfile.py    2025-05-09 
06:44:40.000000000 +0200
+++ new/netpbmfile-2026.1.29/tests/test_netpbmfile.py   2026-01-29 
17:39:04.000000000 +0100
@@ -1,6 +1,6 @@
 # test_netpbmfile.py
 
-# Copyright (c) 2011-2025, Christoph Gohlke
+# Copyright (c) 2011-2026, Christoph Gohlke
 # All rights reserved.
 #
 # Redistribution and use in source and binary forms, with or without
@@ -27,12 +27,9 @@
 # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
 # POSSIBILITY OF SUCH DAMAGE.
 
-# mypy: allow-untyped-defs
-# mypy: check-untyped-defs=False
-
 """Unittests for the netpbmfile package.
 
-:Version: 2025.5.8
+:Version: 2026.1.29
 
 """
 
@@ -42,12 +39,13 @@
 import sys
 import tempfile
 
-import netpbmfile
 import numpy
 import pytest
-from netpbmfile import NetpbmFile, imread, imwrite  # noqa
 from numpy.testing import assert_array_equal
 
+import netpbmfile
+from netpbmfile import NetpbmFile, imread, imwrite
+
 TEST_DIR = os.path.dirname(__file__)
 TEMP_DIR = os.path.join(TEST_DIR, '_tmp')
 
@@ -58,8 +56,8 @@
 class TempFileName:
     """Temporary file name context manager."""
 
-    def __init__(self, name=None, ext='', remove=False):
-        self.remove = remove or TEMP_DIR == tempfile.gettempdir()
+    def __init__(self, name=None, ext='', *, remove=False):
+        self.remove = remove or tempfile.gettempdir() == TEMP_DIR
         if not name:
             with tempfile.NamedTemporaryFile(prefix='test_') as fh:
                 self.name = fh.named
@@ -73,7 +71,7 @@
         if self.remove:
             try:
                 os.remove(self.name)
-            except Exception:
+            except Exception:  # noqa: S110
                 pass
 
 
@@ -329,12 +327,14 @@
 def test_version():
     """Assert netpbmfile versions match docstrings."""
     ver = ':Version: ' + netpbmfile.__version__
+    assert __doc__ is not None
+    assert netpbmfile.__doc__ is not None
     assert ver in __doc__
     assert ver in netpbmfile.__doc__
 
 
 @pytest.mark.parametrize(
-    'name, magicnumber',
+    ('name', 'magicnumber'),
     [
         ('bilevel', 'p1'),
         ('bilevel', 'p4'),
@@ -361,8 +361,8 @@
             data = data.astype('u1')
     else:
         ext = 'pnm'
-    fname = f'{name}{"x4" if multi else ""}.{magicnumber}.{ext}'
-    with TempFileName(fname) as temp:
+    filename = f'{name}{"x4" if multi else ""}.{magicnumber}.{ext}'
+    with TempFileName(filename) as temp:
         magicnumber = magicnumber.upper()
         if not multi:
             data = data[0]
@@ -379,7 +379,7 @@
             if magicnumber not in 'P4 P5 P6':
                 return
             # export PNM as PAM
-            with TempFileName(fname + '.pam') as fn:
+            with TempFileName(filename + '.pam') as fn:
                 fh.write(fn, magicnumber='P7', comment=comment)
                 with NetpbmFile(fn) as fh2:
                     assert fh2.magicnumber == 'P7'
@@ -388,13 +388,15 @@
 
 
 @pytest.mark.parametrize(
-    'fname, magicnumber, dtype, axes, shape, maxval, hash', FILES, ids=idfn
+    ('filename', 'magicnumber', 'dtype', 'axes', 'shape', 'maxval', 'md5hash'),
+    FILES,
+    ids=idfn,
 )
-def test_file(fname, magicnumber, dtype, axes, shape, maxval, hash):
+def test_file(filename, magicnumber, dtype, axes, shape, maxval, md5hash):
     """Verify files can be read and rewritten."""
-    filepath = os.path.join(TEST_DIR, fname)
+    filepath = os.path.join(TEST_DIR, filename)
     if not os.path.exists(filepath):
-        pytest.skip(f'{fname} not found')
+        pytest.skip(f'{filename} not found')
 
     byteorder = '<' if dtype[:2] == '<u' else None
     multitext = magicnumber in 'P1 P2 P3' and axes[0] == 'I'
@@ -422,13 +424,13 @@
         data = fh.asarray()
         if not multitext:
             assert data.shape == shape
-        assert md5(data) == hash
+        assert md5(data) == md5hash
 
     if magicnumber in 'PF4 Pf P7 332':
         return
 
     # rewrite
-    with TempFileName(fname) as temp:
+    with TempFileName(filename) as temp:
         imwrite(
             temp,
             data,
@@ -484,7 +486,7 @@
 
     # rewrite as P7
     magicnumber = 'P7'
-    with TempFileName(fname + '.pam') as temp:
+    with TempFileName(filename + '.pam') as temp:
         imwrite(
             temp,
             data,
@@ -538,9 +540,9 @@
     except ImportError as exc:
         pytest.skip(exc.msg)
 
-    fname = os.path.join(TEST_DIR, 'P4_multi.pbm')
-    data = imread(fname)
-    with LfdFile(fname) as lfd:
+    filename = os.path.join(TEST_DIR, 'P4_multi.pbm')
+    data = imread(filename)
+    with LfdFile(filename) as lfd:
         assert_array_equal(data, lfd.asarray())
 
 
@@ -554,3 +556,6 @@
     argv.append('--cov=netpbmfile')
     argv.append('--verbose')
     sys.exit(pytest.main(argv))
+
+# mypy: allow-untyped-defs
+# mypy: check-untyped-defs=False

Reply via email to