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