Script 'mail_helper' called by obssrc
Hello community,

here is the log from the commit of package python-oiffile for openSUSE:Factory 
checked in at 2023-01-28 18:43:02
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Comparing /work/SRC/openSUSE:Factory/python-oiffile (Old)
 and      /work/SRC/openSUSE:Factory/.python-oiffile.new.32243 (New)
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Package is "python-oiffile"

Sat Jan 28 18:43:02 2023 rev:4 rq:1061494 version:2022.9.29

Changes:
--------
--- /work/SRC/openSUSE:Factory/python-oiffile/python-oiffile.changes    
2021-02-19 23:45:55.863402554 +0100
+++ /work/SRC/openSUSE:Factory/.python-oiffile.new.32243/python-oiffile.changes 
2023-01-28 19:01:56.376258173 +0100
@@ -1,0 +2,15 @@
+Thu Jan 26 23:00:56 UTC 2023 - Ben Greiner <c...@bnavigator.de>
+
+- Update to 2022.9.29
+  * Switch to Google style docstrings.
+- Release 2022.2.2
+  * Add type hints.
+  * Add main function.
+  * Add FileSystemAbc abstract base class.
+  * Remove OifFile.tiffs (breaking).
+  * Drop support for Python 3.7 and numpy < 1.19 (NEP29).
+- Release 2021.6.6
+  * Fix unclosed file warnings.
+- Clean specfile
+
+-------------------------------------------------------------------

Old:
----
  oiffile-2020.9.18.tar.gz

New:
----
  oiffile-2022.9.29.tar.gz

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

Other differences:
------------------
++++++ python-oiffile.spec ++++++
--- /var/tmp/diff_new_pack.YF3wsB/_old  2023-01-28 19:01:56.764260313 +0100
+++ /var/tmp/diff_new_pack.YF3wsB/_new  2023-01-28 19:01:56.768260335 +0100
@@ -1,7 +1,7 @@
 #
 # spec file for package python-oiffile
 #
-# Copyright (c) 2021 SUSE LLC
+# Copyright (c) 2023 SUSE LLC
 #
 # All modifications and additions to the file contributed by third parties
 # remain the property of their copyright owners, unless otherwise agreed
@@ -16,25 +16,25 @@
 #
 
 
-%define packagename oiffile
-%define skip_python2 1
-%define skip_python36 1
-%{?!python_module:%define python_module() python-%{**} python3-%{**}}
 Name:           python-oiffile
-Version:        2020.9.18
+Version:        2022.9.29
 Release:        0
 Summary:        Read Olympus(r) image files (OIF and OIB)
 License:        BSD-3-Clause
 Group:          Development/Languages/Python
 URL:            https://www.lfd.uci.edu/~gohlke/
-Source:         
https://github.com/cgohlke/oiffile/archive/v%{version}.tar.gz#/%{packagename}-%{version}.tar.gz
-BuildRequires:  %{python_module numpy >= 1.15}
+# SourceRepository: https://github.com/cgohlke/oiffile
+Source:         
https://github.com/cgohlke/oiffile/archive/v%{version}.tar.gz#/oiffile-%{version}.tar.gz
+BuildRequires:  %{python_module base >= 3.8}
+BuildRequires:  %{python_module numpy >= 1.19.2}
+BuildRequires:  %{python_module pip}
 BuildRequires:  %{python_module setuptools}
-BuildRequires:  %{python_module tifffile >= 2020.6.3}
+BuildRequires:  %{python_module tifffile >= 2021.11.2}
+BuildRequires:  %{python_module wheel}
 BuildRequires:  fdupes
 BuildRequires:  python-rpm-macros
-Requires:       python-numpy >= 1.15
-Requires:       python-tifffile >= 2020.6.3
+Requires:       python-numpy >= 1.19.2
+Requires:       python-tifffile >= 2021.11.2
 BuildArch:      noarch
 %python_subpackages
 
@@ -44,15 +44,15 @@
 software for confocal microscopy.
 
 %prep
-%setup -q -n %{packagename}-%{version}
+%setup -q -n oiffile-%{version}
 # Fix warning: wrong end-of-line encoding
 sed -i 's/\r//g' README.rst
 
 %build
-%python_build
+%pyproject_wheel
 
 %install
-%python_install
+%pyproject_install
 %python_expand %fdupes %{buildroot}%{$python_sitelib}
 
 %check
@@ -61,7 +61,7 @@
 %files %{python_files}
 %doc README.rst
 %license LICENSE
-%{python_sitelib}/*egg-info/
-%{python_sitelib}/%{packagename}/
+%{python_sitelib}/oiffile
+%{python_sitelib}/oiffile-%{version}.dist-info
 
 %changelog

++++++ oiffile-2020.9.18.tar.gz -> oiffile-2022.9.29.tar.gz ++++++
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/oiffile-2020.9.18/LICENSE 
new/oiffile-2022.9.29/LICENSE
--- old/oiffile-2020.9.18/LICENSE       2020-09-19 06:59:59.000000000 +0200
+++ new/oiffile-2022.9.29/LICENSE       2022-09-30 07:53:32.000000000 +0200
@@ -1,6 +1,6 @@
 BSD 3-Clause License
 
-Copyright (c) 2012-2020, Christoph Gohlke
+Copyright (c) 2012-2022, 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/oiffile-2020.9.18/README.rst 
new/oiffile-2022.9.29/README.rst
--- old/oiffile-2020.9.18/README.rst    2020-09-19 06:59:59.000000000 +0200
+++ new/oiffile-2022.9.29/README.rst    2022-09-30 07:53:32.000000000 +0200
@@ -7,45 +7,66 @@
 
 There are two variants of the format:
 
-* OIF (Olympus Image File) is a multi-file format that includes a main setting
+- OIF (Olympus Image File) is a multi-file format that includes a main setting
   file (.oif) and an associated directory with data and setting files (.tif,
-  .bmp, .txt, .pyt, .roi, and .lut).
+  .bmp, .txt, .pty, .roi, and .lut).
 
-* OIB (Olympus Image Binary) is a compound document file, storing OIF and
+- OIB (Olympus Image Binary) is a compound document file, storing OIF and
   associated files within a single file.
 
-:Author:
-  `Christoph Gohlke <https://www.lfd.uci.edu/~gohlke/>`_
-
-:Organization:
-  Laboratory for Fluorescence Dynamics. University of California, Irvine
-
+:Author: `Christoph Gohlke <https://www.cgohlke.com>`_
 :License: BSD 3-Clause
-
-:Version: 2020.9.18
+:Version: 2022.9.29
 
 Requirements
 ------------
-* `CPython >= 3.7 <https://www.python.org>`_
-* `Numpy 1.15 <https://www.numpy.org>`_
-* `Tifffile 2020.6.3 <https://pypi.org/project/tifffile/>`_
+
+This release has been tested with the following requirements and dependencies
+(other versions may work):
+
+- `CPython 3.8.10, 3.9.13, 3.10.7, 3.11.0rc2 <https://www.python.org>`_
+- `Numpy 1.22.4 <https://pypi.org/project/numpy/>`_
+- `Tifffile 2022.8.12 <https://pypi.org/project/tifffile/>`_
 
 Revisions
 ---------
+
+2022.9.29
+
+- Switch to Google style docstrings.
+
+2022.2.2
+
+- Add type hints.
+- Add main function.
+- Add FileSystemAbc abstract base class.
+- Remove OifFile.tiffs (breaking).
+- Drop support for Python 3.7 and numpy < 1.19 (NEP29).
+
+2021.6.6
+
+- Fix unclosed file warnings.
+
 2020.9.18
-    Remove support for Python 3.6 (NEP 29).
-    Support os.PathLike file names.
-    Fix unclosed files.
+
+- Remove support for Python 3.6 (NEP 29).
+- Support os.PathLike file names.
+- Fix unclosed files.
+
 2020.1.18
-    Fix indentation error.
+
+- Fix indentation error.
+
 2020.1.1
-    Support multiple image series.
-    Parse shape and dtype from settings file.
-    Remove support for Python 2.7 and 3.5.
-    Update copyright.
+
+- Support multiple image series.
+- Parse shape and dtype from settings file.
+- Remove support for Python 2.7 and 3.5.
+- Update copyright.
 
 Notes
 -----
+
 The API is not stable yet and might change between revisions.
 
 No specification document is available.
@@ -88,6 +109,7 @@
 
 Extract the OIB file content to an OIF file and associated data directory:
 
+>>> import tempfile
 >>> tempdir = tempfile.mkdtemp()
 >>> oib2oif('test.oib', location=tempdir)
 Saving ... done.
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/oiffile-2020.9.18/oiffile/__main__.py 
new/oiffile-2022.9.29/oiffile/__main__.py
--- old/oiffile-2020.9.18/oiffile/__main__.py   1970-01-01 01:00:00.000000000 
+0100
+++ new/oiffile-2022.9.29/oiffile/__main__.py   2022-09-30 07:53:32.000000000 
+0200
@@ -0,0 +1,9 @@
+# oiffile/__main__.py
+
+"""Oiffile package command line script."""
+
+import sys
+
+from .oiffile import main
+
+sys.exit(main())
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/oiffile-2020.9.18/oiffile/oiffile.py 
new/oiffile-2022.9.29/oiffile/oiffile.py
--- old/oiffile-2020.9.18/oiffile/oiffile.py    2020-09-19 06:59:59.000000000 
+0200
+++ new/oiffile-2022.9.29/oiffile/oiffile.py    2022-09-30 07:53:32.000000000 
+0200
@@ -1,6 +1,6 @@
 # oiffile.py
 
-# Copyright (c) 2012-2020, Christoph Gohlke
+# Copyright (c) 2012-2022, Christoph Gohlke
 # All rights reserved.
 #
 # Redistribution and use in source and binary forms, with or without
@@ -37,45 +37,66 @@
 
 There are two variants of the format:
 
-* OIF (Olympus Image File) is a multi-file format that includes a main setting
+- OIF (Olympus Image File) is a multi-file format that includes a main setting
   file (.oif) and an associated directory with data and setting files (.tif,
-  .bmp, .txt, .pyt, .roi, and .lut).
+  .bmp, .txt, .pty, .roi, and .lut).
 
-* OIB (Olympus Image Binary) is a compound document file, storing OIF and
+- OIB (Olympus Image Binary) is a compound document file, storing OIF and
   associated files within a single file.
 
-:Author:
-  `Christoph Gohlke <https://www.lfd.uci.edu/~gohlke/>`_
-
-:Organization:
-  Laboratory for Fluorescence Dynamics. University of California, Irvine
-
+:Author: `Christoph Gohlke <https://www.cgohlke.com>`_
 :License: BSD 3-Clause
-
-:Version: 2020.9.18
+:Version: 2022.9.29
 
 Requirements
 ------------
-* `CPython >= 3.7 <https://www.python.org>`_
-* `Numpy 1.15 <https://www.numpy.org>`_
-* `Tifffile 2020.6.3 <https://pypi.org/project/tifffile/>`_
+
+This release has been tested with the following requirements and dependencies
+(other versions may work):
+
+- `CPython 3.8.10, 3.9.13, 3.10.7, 3.11.0rc2 <https://www.python.org>`_
+- `Numpy 1.22.4 <https://pypi.org/project/numpy/>`_
+- `Tifffile 2022.8.12 <https://pypi.org/project/tifffile/>`_
 
 Revisions
 ---------
+
+2022.9.29
+
+- Switch to Google style docstrings.
+
+2022.2.2
+
+- Add type hints.
+- Add main function.
+- Add FileSystemAbc abstract base class.
+- Remove OifFile.tiffs (breaking).
+- Drop support for Python 3.7 and numpy < 1.19 (NEP29).
+
+2021.6.6
+
+- Fix unclosed file warnings.
+
 2020.9.18
-    Remove support for Python 3.6 (NEP 29).
-    Support os.PathLike file names.
-    Fix unclosed files.
+
+- Remove support for Python 3.6 (NEP 29).
+- Support os.PathLike file names.
+- Fix unclosed files.
+
 2020.1.18
-    Fix indentation error.
+
+- Fix indentation error.
+
 2020.1.1
-    Support multiple image series.
-    Parse shape and dtype from settings file.
-    Remove support for Python 2.7 and 3.5.
-    Update copyright.
+
+- Support multiple image series.
+- Parse shape and dtype from settings file.
+- Remove support for Python 2.7 and 3.5.
+- Update copyright.
 
 Notes
 -----
+
 The API is not stable yet and might change between revisions.
 
 No specification document is available.
@@ -118,6 +139,7 @@
 
 Extract the OIB file content to an OIF file and associated data directory:
 
+>>> import tempfile
 >>> tempdir = tempfile.mkdtemp()
 >>> oib2oif('test.oib', location=tempdir)
 Saving ... done.
@@ -140,22 +162,27 @@
 
 """
 
-__version__ = '2020.9.18'
+from __future__ import annotations
+
+__version__ = '2022.9.29'
 
-__all__ = (
+__all__ = [
     'imread',
     'oib2oif',
     'OifFile',
     'OifFileError',
     'OibFileSystem',
     'OifFileSystem',
+    'FileSystemAbc',
     'SettingsFile',
     'CompoundFile',
     'filetime',
-)
+]
 
 import os
+import sys
 import re
+import abc
 import struct
 from io import BytesIO
 from glob import glob
@@ -163,22 +190,50 @@
 
 import numpy
 
-from tifffile import TiffFile, TiffSequence, lazyattr, natural_sorted
+from tifffile import TiffFile, TiffSequence, natural_sorted
+
+from typing import TYPE_CHECKING
+
+if TYPE_CHECKING:
+    from typing import (
+        Any,
+        BinaryIO,
+        Generator,
+        Literal,
+        Iterable,
+        Iterator,
+    )
 
 
-def imread(filename, *args, **kwargs):
-    """Return image data from OIF or OIB file as numpy array.
+def imread(filename: str | os.PathLike, /, **kwargs) -> numpy.ndarray:
+    """Return image data from OIF or OIB file.
 
-    'args' and 'kwargs' are arguments to OifFile.asarray().
+    Parameters:
+        filename:
+            Path to OIB or OIF file.
+        **kwargs:
+            Additional arguments passed to :py:meth:`OifFile.asarray`.
 
     """
     with OifFile(filename) as oif:
-        result = oif.asarray(*args, **kwargs)
+        result = oif.asarray(**kwargs)
     return result
 
 
-def oib2oif(filename, location='', verbose=1):
-    """Convert OIB file to OIF."""
+def oib2oif(
+    filename: str | os.PathLike, /, location: str = '', *, verbose: int = 1
+) -> None:
+    """Convert OIB file to OIF.
+
+    Parameters:
+        filename:
+            Name of OIB file to convert.
+        location:
+            Directory, where files are written.
+        verbose:
+            Level of printed status messages.
+
+    """
     with OibFileSystem(filename) as oib:
         oib.saveas_oif(location=location, verbose=verbose)
 
@@ -190,36 +245,41 @@
 class OifFile:
     """Olympus Image File.
 
-    Attributes
-    ----------
-    mainfile : SettingsFile
-        The main OIF settings.
-    filesystem : OibFileSystem or OifFileSystem
-        The underlying file system instance.
-    series : tuple of tifffile.TiffSequence
-        Sequence of TIFF files. Includes shape, dtype, and axes information.
-    is_oib : bool
-        True if OIB file.
+    Parameters:
+        filename: Path to OIB or OIF file.
 
     """
 
-    def __init__(self, filename):
-        """Open OIF or OIB file and read main settings."""
+    filename: str
+    """Name of OIB or OIF file."""
+    filesystem: FileSystemAbc
+    """Underlying file system instance."""
+    mainfile: SettingsFile
+    """Main settings."""
+
+    _files_flat: dict[str, str]
+    _series: tuple[TiffSequence, ...] | None
+
+    def __init__(self, filename: str | os.PathLike, /) -> None:
         self.filename = filename = os.fspath(filename)
         if filename.lower().endswith('.oif'):
             self.filesystem = OifFileSystem(filename)
-            self.is_oib = False
         else:
             self.filesystem = OibFileSystem(filename)
-            self.is_oib = True
         self.mainfile = self.filesystem.settings
         # map file names to storage names (flattened name space)
         self._files_flat = {
             os.path.basename(f): f for f in self.filesystem.files()
         }
+        self._series = None
+
+    def open_file(self, filename: str, /) -> BinaryIO:
+        """Return open file object from path name.
 
-    def open_file(self, filename):
-        """Return open file object from path name."""
+        Parameters:
+            filename: Name of file to open.
+
+        """
         try:
             return self.filesystem.open_file(
                 self._files_flat.get(filename, filename)
@@ -227,22 +287,39 @@
         except (KeyError, OSError) as exc:
             raise FileNotFoundError(f'No such file: {filename}') from exc
 
-    def glob(self, pattern='*'):
-        """Return iterator over unsorted file names matching pattern."""
+    def glob(self, pattern: str = '*', /) -> Iterator[str]:
+        """Return iterator over unsorted file names matching pattern.
+
+        Parameters:
+            pattern: File glob pattern.
+
+        """
         if pattern == '*':
             return self.filesystem.files()
-        pattern = pattern.replace('.', r'\.').replace('*', '.*')
-        pattern = re.compile(pattern)
-        return (f for f in self.filesystem.files() if pattern.match(f))
+        ptrn = re.compile(pattern.replace('.', r'\.').replace('*', '.*'))
+        return (f for f in self.filesystem.files() if ptrn.match(f))
+
+    @property
+    def is_oib(self) -> bool:
+        """File has OIB format."""
+        return isinstance(self.filesystem, OibFileSystem)
 
     @property
-    def axes(self):
-        """Return order of axes in image data from mainfile."""
+    def axes(self) -> str:
+        """Character codes for dimensions in image array according to mainfile.
+
+        This might differ from the axes order of series.
+
+        """
         return self.mainfile['Axis Parameter Common']['AxisOrder'][::-1]
 
     @property
-    def shape(self):
-        """Return shape of image data from mainfile."""
+    def shape(self) -> tuple[int, ...]:
+        """Shape of image data according to mainfile.
+
+        This might differ from the shape of series.
+
+        """
         size = {
             self.mainfile[f'Axis {i} Parameters Common']['AxisCode']: int(
                 self.mainfile[f'Axis {i} Parameters Common']['MaxSize']
@@ -252,51 +329,53 @@
         return tuple(size[ax] for ax in self.axes)
 
     @property
-    def dtype(self):
-        """Return dtype of image data from mainfile."""
+    def dtype(self) -> numpy.dtype:
+        """Type of image data according to mainfile.
+
+        This might differ from the dtype of series.
+
+        """
         bitcount = int(
             self.mainfile['Reference Image Parameter']['ValidBitCounts']
         )
         return numpy.dtype('<u2' if bitcount > 8 else '<u2')
 
-    @lazyattr
-    def series(self):
-        """Return tuple of TiffSequence of sorted TIFF files."""
-        series = {}
+    @property
+    def series(self) -> tuple[TiffSequence, ...]:
+        """Sequence of series of TIFF files with matching names."""
+        if self._series is not None:
+            return self._series
+        tiffiles: dict[str, list[str]] = {}
         for fname in self.glob('*.tif'):
             key = ''.join(
                 c for c in os.path.split(fname)[-1][:-4] if c.isalpha()
             )
-            if key in series:
-                series[key].append(fname)
+            if key in tiffiles:
+                tiffiles[key].append(fname)
             else:
-                series[key] = [fname]
-        series = [
+                tiffiles[key] = [fname]
+        series = tuple(
             TiffSequence(
                 natural_sorted(files), imread=self.asarray, pattern='axes'
             )
-            for files in series.values()
-        ]
+            for files in tiffiles.values()
+        )
         if len(series) > 1:
             series = tuple(
                 reversed(sorted(series, key=lambda x: len(x.files)))
             )
+        self._series = series
         return series
 
-    @property
-    def tiffs(self):
-        """Return first TiffSequence."""
-        # required for compatibility with cmapfile < 2020.1.1
-        return self.series[0]
-
-    def asarray(self, series=0, **kwargs):
+    def asarray(self, series: int | str = 0, **kwargs) -> numpy.ndarray:
         """Return image data from TIFF file(s) as numpy array.
 
-        By default the data from the TIFF files in the first image series
-        is returned.
-
-        The kwargs parameters are passed to the asarray functions of the
-        TiffFile or TiffSequence instances.
+        Parameters:
+            series:
+                Specifies which series to return as array.
+            kwargs:
+                Additional parameters passed to :py:meth:`TiffFile.asarray`
+                or :py:meth:`TiffSequence.asarray`.
 
         """
         if isinstance(series, int):
@@ -309,39 +388,91 @@
             fh.close()
         return result
 
-    def close(self):
+    def close(self) -> None:
         """Close file handle."""
         self.filesystem.close()
 
-    def __enter__(self):
+    def __enter__(self) -> OifFile:
         return self
 
     def __exit__(self, exc_type, exc_value, traceback):
         self.close()
 
-    def __str__(self):
-        """Return string with information about OifFile."""
+    def __repr__(self) -> str:
+        filename = os.path.split(self.filename)[-1]
+        return f'<{self.__class__.__name__} {filename!r}>'
+
+    def __str__(self) -> str:
         # info = self.mainfile['Version Info']
-        s = [
-            self.__class__.__name__,
-            os.path.normpath(os.path.normcase(self.filename)),
+        return indent(
+            repr(self),
             f'axes: {self.axes}',
-            'shape: {}'.format(', '.join(str(i) for i in self.shape)),
+            f'shape: {self.shape}',
             f'dtype: {self.dtype}',
             # f'system name: {info.get("SystemName", "None")}',
             # f'system version: {info.get("SystemVersion", "None")}',
             # f'file version: {info.get("FileVersion", "None")}',
-        ]
-        if len(self.series) > 1:
-            s.append(f'series: {len(self.series)}')
-        return '\n '.join(s)
+            # indent(f'series: {len(self.series)}', *self.series),
+            f'series: {len(self.series)}',
+        )
+
+
+class FileSystemAbc(metaclass=abc.ABCMeta):
+    """Abstract base class for structures with key."""
+
+    filename: str
+    """Name of OIB or OIF file."""
+    name: str
+    """Name from settings file."""
+    version: str
+    """Version from settings file."""
+    mainfile: str
+    """Name of main settings file."""
+    settings: SettingsFile
+    """Main settings."""
+
+    @abc.abstractmethod
+    def open_file(self, filename: str, /) -> BinaryIO:
+        """Return file object from path name.
+
+        Parameters:
+            filename: Name of file to open.
+
+        """
+
+    @abc.abstractmethod
+    def files(self) -> Iterator[str]:
+        """Return iterator over unsorted files in FileSystem."""
+
+    def close(self) -> None:
+        """Close file handle."""
+
+    def __repr__(self) -> str:
+        return (
+            f'<{self.__class__.__name__} {os.path.split(self.filename)[-1]!r}>'
+        )
+
 
+class OifFileSystem(FileSystemAbc):
+    """Olympus Image File file system.
 
-class OifFileSystem:
-    """Olympus Image File file system."""
+    Parameters:
+        filename:
+            Name of OIF file.
+        storage_ext:
+            Name extension of storage directory.
 
-    def __init__(self, filename, storage_ext='.files'):
-        """Open OIF file and read settings."""
+    """
+
+    filename: str
+    name: str
+    version: str
+    mainfile: str
+    settings: SettingsFile
+    _files: list[str]
+    _path: str
+
+    def __init__(self, filename: str | os.PathLike, /, storage_ext='.files'):
         self.filename = filename = os.fspath(filename)
         self._path, self.mainfile = os.path.split(os.path.abspath(filename))
         self.settings = SettingsFile(filename, name=self.mainfile)
@@ -359,51 +490,59 @@
         for f in glob(os.path.join(storage, '*')):
             self._files.append(f[pathlen:])
 
-    def open_file(self, filename):
+    def open_file(self, filename: str, /) -> BinaryIO:
         """Return file object from path name.
 
-        File object must be closed.
+        The returned file object must be closed by the user.
+
+        Parameters:
+            filename: Name of file to open.
 
         """
         return open(os.path.join(self._path, filename), 'rb')
 
-    def files(self):
+    def files(self) -> Iterator[str]:
         """Return iterator over unsorted files in OIF."""
         return iter(self._files)
 
-    def glob(self, pattern):
-        """Return iterator of path names matching the specified pattern."""
-        pattern = pattern.replace('.', r'\.').replace('*', '.*')
-        pattern = re.compile(pattern)
-        return (f for f in self.files() if pattern.match(f))
-
-    def close(self):
+    def close(self) -> None:
         """Close file handle."""
 
-    def __enter__(self):
+    def __enter__(self) -> OifFileSystem:
         return self
 
     def __exit__(self, exc_type, exc_value, traceback):
         self.close()
 
-    def __str__(self):
-        """Return string with information about OifFileSystem."""
-        return '\n '.join(
-            (
-                self.__class__.__name__,
-                os.path.normpath(os.path.normcase(self.filename)),
-                f'name: {self.name}',
-                f'version: {self.version}',
-                f'mainfile: {self.mainfile}',
-            )
+    def __str__(self) -> str:
+        return indent(
+            repr(self),
+            f'name: {self.name}',
+            f'version: {self.version}',
+            f'mainfile: {self.mainfile}',
         )
 
 
-class OibFileSystem:
-    """Olympus Image Binary file system."""
+class OibFileSystem(FileSystemAbc):
+    """Olympus Image Binary file system.
+
+    Parameters:
+        filename: Name of OIB file.
+
+    """
 
-    def __init__(self, filename):
-        """Open compound document and read OibInfo.txt settings."""
+    filename: str
+    name: str
+    version: str
+    mainfile: str
+    settings: SettingsFile
+    com: CompoundFile
+    compression: str
+    _files: dict[str, str]
+    _folders: dict[str, str]
+
+    def __init__(self, filename: str | os.PathLike, /) -> None:
+        # open compound document and read OibInfo.txt settings
         self.filename = filename = os.fspath(filename)
         self.com = CompoundFile(filename)
         info = SettingsFile(self.com.open_file('OibInfo.txt'), 'OibInfo.txt')[
@@ -427,18 +566,23 @@
             self.open_file(self.mainfile), name=self.mainfile
         )
 
-    def open_file(self, filename):
-        """Return file object from case sensitive path name."""
+    def open_file(self, filename: str, /) -> BinaryIO:
+        """Return file object from case sensitive path name.
+
+        Parameters:
+            filename: Name of file to open.
+
+        """
         try:
             return self.com.open_file(self._files[filename])
         except KeyError as exc:
             raise FileNotFoundError(f'No such file: {filename}') from exc
 
-    def files(self):
+    def files(self) -> Iterator[str]:
         """Return iterator over unsorted files in OIB."""
         return iter(self._files.keys())
 
-    def saveas_oif(self, location='', verbose=0):
+    def saveas_oif(self, location: str = '', *, verbose: int = 0) -> None:
         """Save all streams in OIB file as separate files.
 
         Raise OSError if target files or directories already exist.
@@ -446,6 +590,12 @@
         The main .oif file name and storage names are determined from the
         OibInfo.txt settings.
 
+        Parameters:
+            location:
+                Directory, where files are written.
+            verbose:
+                Level of printed status messages.
+
         """
         if location and not os.path.exists(location):
             os.makedirs(location)
@@ -472,32 +622,28 @@
         if verbose == 1:
             print(' done.')
 
-    def close(self):
+    def close(self) -> None:
         """Close file handle."""
         self.com.close()
 
-    def __enter__(self):
+    def __enter__(self) -> OibFileSystem:
         return self
 
     def __exit__(self, exc_type, exc_value, traceback):
         self.close()
 
-    def __str__(self):
-        """Return string with information about OibFileSystem."""
-        return '\n '.join(
-            (
-                self.__class__.__name__,
-                os.path.normpath(os.path.normcase(self.filename)),
-                f'name: {self.name}',
-                f'version: {self.version}',
-                f'mainfile: {self.mainfile}',
-                f'compression: {self.compression}',
-            )
+    def __str__(self) -> str:
+        return indent(
+            repr(self),
+            f'name: {self.name}',
+            f'version: {self.version}',
+            f'mainfile: {self.mainfile}',
+            f'compression: {self.compression}',
         )
 
 
 class SettingsFile(dict):
-    """Olympus settings file (oif, txt, pyt, roi, lut).
+    """Olympus settings file (oif, txt, pty, roi, lut).
 
     Settings files contain little endian utf-16 encoded strings, except for
     [ColorLUTData] sections, which contain uint8 binary arrays.
@@ -505,28 +651,31 @@
     Settings can be accessed as a nested dictionary {section: {key: value}},
     except for {'ColorLUTData': numpy array}.
 
-    """
-
-    def __init__(self, arg, name=None):
-        """Read settings file and parse into nested dictionaries.
-
-        Parameters
-        ----------
-        arg : str or file object
+    Parameters:
+        arg:
             Name of file or open file containing little endian UTF-16 string.
-            File objects are closed by this function.
-        name : str
+            File objects are closed.
+        name:
             Human readable label of stream.
 
-        """
+    """
+
+    name: str
+    """Name of settings."""
+
+    def __init__(self, arg, /, name: str | None = None) -> None:
+        # read settings file and parse into nested dictionaries
         dict.__init__(self)
         if isinstance(arg, (str, os.PathLike)):
-            self.name = os.fspath(arg)
+            self.name = os.path.split(arg)[-1]
             stream = open(arg, 'rb')
         else:
             self.name = str(name)
             stream = arg
 
+        content: bytes
+        content_list: list[bytes]
+
         try:
             content = stream.read()
         finally:
@@ -534,31 +683,31 @@
 
         if content[:4] == b'\xFF\xFE\x5B\x00':
             # UTF16 BOM
-            content = content.rsplit(
+            content_list = content.rsplit(
                 b'[\x00C\x00o\x00l\x00o\x00r\x00L\x00U\x00T\x00'
                 b'D\x00a\x00t\x00a\x00]\x00\x0D\x00\x0A\x00',
                 1,
             )
-            if len(content) > 1:
+            if len(content_list) > 1:
                 self['ColorLUTData'] = numpy.fromstring(
-                    content[1], 'uint8'
+                    content_list[1], dtype=numpy.uint8  # type: ignore
                 ).reshape(-1, 4)
-            content = content[0].decode('utf-16')
+            contents = content_list[0].decode('utf-16')
         elif content[:1] == b'[':
             # try UTF-8
-            content = content.rsplit(b'[ColorLUTData]\r\n', 1)
-            if len(content) > 1:
+            content_list = content.rsplit(b'[ColorLUTData]\r\n', 1)
+            if len(content_list) > 1:
                 self['ColorLUTData'] = numpy.fromstring(
-                    content[1], 'uint8'
+                    content_list[1], dtype=numpy.uint8  # type: ignore
                 ).reshape(-1, 4)
             try:
-                content = content[0].decode()
+                contents = content_list[0].decode()
             except Exception as exc:
                 raise ValueError('not a valid settings file') from exc
         else:
             raise ValueError('not a valid settings file')
 
-        for line in content.splitlines():
+        for line in contents.splitlines():
             line = line.strip()
             if line.startswith(';'):
                 continue
@@ -568,9 +717,11 @@
                 key, value = line.split('=')
                 properties[key] = astype(value)
 
-    def __str__(self):
-        """Return string with information about SettingsFile."""
-        return '\n '.join((self.__class__.__name__, format_dict(self)))
+    def __repr__(self) -> str:
+        return f'<{self.__class__.__name__} {self.name!r}>'
+
+    def __str__(self) -> str:
+        return indent(repr(self), format_dict(self))
 
 
 class CompoundFile:
@@ -581,8 +732,32 @@
 
     This should be able to read Olympus OIB and Zeiss ZVI files.
 
+    Parameters:
+        filename: Path to compound document file.
+
     """
 
+    filename: str
+    clsid: bytes | None
+    version_minor: int
+    version_major: int
+    byteorder: Literal['<']
+    dir_len: int
+    fat_len: int
+    dir_start: int
+    mini_stream_cutof_size: int
+    minifat_start: int
+    minifat_len: int
+    difat_start: int
+    difat_len: int
+    sec_size: int
+    short_sec_size: int
+    _files: dict[str, DirectoryEntry]
+    _fat: list[Any]
+    _minifat: list[Any]
+    _difat: list[Any]
+    _dirs: list[DirectoryEntry]
+
     MAXREGSECT = 0xFFFFFFFA
     DIFSECT = 0xFFFFFFFC
     FATSECT = 0xFFFFFFFD
@@ -591,12 +766,18 @@
     MAXREGSID = 0xFFFFFFFA
     NOSTREAM = 0xFFFFFFFF
 
-    def __init__(self, filename):
-        """Initialize instance from file."""
+    def __init__(self, filename: str | os.PathLike, /) -> None:
         self.filename = filename = os.fspath(filename)
         self._fh = open(filename, 'rb')
+        try:
+            self._fromfile()
+        except Exception:
+            self._fh.close()
+            raise
+
+    def _fromfile(self) -> None:
+        """Initialize instance from file."""
         if self._fh.read(8) != b'\xd0\xcf\x11\xe0\xa1\xb1\x1a\xe1':
-            self.close()
             raise ValueError('not a compound document file')
         (
             self.clsid,
@@ -618,14 +799,17 @@
             self.difat_len,
         ) = struct.unpack('<16sHHHHHHIIIIIIIIII', self._fh.read(68))
 
-        self.byteorder = {0xFFFE: '<', 0xFEFF: '>'}[byteorder]
-        if self.byteorder != '<':
+        if byteorder == 0xFFFE:
+            self.byteorder = '<'
+        else:
+            # 0xFEFF
+            # self.byteorder = '>'
             raise NotImplementedError('big-endian byte order not supported')
 
         if self.clsid == b'\x00' * 16:
             self.clsid = None
         if self.clsid is not None:
-            raise OifFileError(f'cannot handle clsid {self.clsid}')
+            raise OifFileError(f'cannot handle clsid {self.clsid!r}')
 
         if self.version_minor != 0x3E:
             raise OifFileError(
@@ -648,8 +832,8 @@
                 f'and sector_shift {sector_shift}'
             )
 
-        self.sec_size = 2 ** sector_shift
-        self.short_sec_size = 2 ** mini_sector_shift
+        self.sec_size = 2**sector_shift
+        self.short_sec_size = 2**mini_sector_shift
 
         secfmt = '<' + ('I' * (self.sec_size // 4))
         # read DIFAT
@@ -688,7 +872,7 @@
             raise OifFileError('no directories found')
         root = self._dirs[0]
         if root.name != 'Root Entry':
-            raise OifFileError(f"no root directory found, got {root.name}")
+            raise OifFileError(f'no root directory found, got {root.name}')
         if root.create_time is not None:  # and root.modify_time is None
             raise OifFileError(f'invalid root.create_time {root.create_time}')
         if root.stream_size % self.short_sec_size != 0:
@@ -705,7 +889,9 @@
         dirs = self._dirs
         visited = [False] * len(self._dirs)
 
-        def parse(dirid, path):
+        def parse(
+            dirid: int, path: list[str]
+        ) -> Generator[tuple[str, DirectoryEntry], None, None]:
             # return iterator over all file names and their directory entries
             # TODO: replace with red-black tree parser
             if dirid == nostream or visited[dirid]:
@@ -721,7 +907,7 @@
 
         self._files = dict(parse(self._dirs[0].child_id, []))
 
-    def _read_stream(self, direntry):
+    def _read_stream(self, direntry: DirectoryEntry, /) -> bytes:
         """Return content of stream."""
         if direntry.stream_size < self.mini_stream_cutof_size:
             result = b''.join(self._mini_sec_chain(direntry.sector_start))
@@ -729,82 +915,103 @@
             result = b''.join(self._sec_chain(direntry.sector_start))
         return result[: direntry.stream_size]
 
-    def _sec_read(self, secid):
+    def _sec_read(self, secid: int, /) -> bytes:
         """Return content of sector from file."""
         self._fh.seek(self.sec_size + secid * self.sec_size)
         return self._fh.read(self.sec_size)
 
-    def _sec_chain(self, secid):
+    def _sec_chain(self, secid: int, /) -> Generator[bytes, None, None]:
         """Return iterator over FAT sector chain content."""
         while secid != CompoundFile.ENDOFCHAIN:
             if secid <= CompoundFile.MAXREGSECT:
                 yield self._sec_read(secid)
             secid = self._fat[secid]
 
-    def _mini_sec_read(self, secid):
+    def _mini_sec_read(self, secid: int, /) -> bytes:
         """Return content of sector from mini stream."""
         pos = secid * self.short_sec_size
         return self._ministream[pos : pos + self.short_sec_size]
 
-    def _mini_sec_chain(self, secid):
+    def _mini_sec_chain(self, secid: int, /) -> Generator[bytes, None, None]:
         """Return iterator over mini FAT sector chain content."""
         while secid != CompoundFile.ENDOFCHAIN:
             if secid <= CompoundFile.MAXREGSECT:
                 yield self._mini_sec_read(secid)
             secid = self._minifat[secid]
 
-    def files(self):
+    def files(self) -> Iterable[str]:
         """Return sequence of file names."""
         return self._files.keys()
 
-    def direntry(self, name):
-        """Return DirectoryEntry of filename."""
-        return self._files[name]
+    def direntry(self, filename: str, /) -> DirectoryEntry:
+        """Return DirectoryEntry of filename.
+
+        Parameters:
+            filename: Name of file.
+
+        """
+        return self._files[filename]
+
+    def open_file(self, filename: str, /) -> BytesIO:
+        """Return stream as file like object.
+
+        Parameters:
+            filename: Name of file to open.
 
-    def open_file(self, filename):
-        """Return stream as file like object."""
+        """
         return BytesIO(self._read_stream(self._files[filename]))
 
-    def format_tree(self):
+    def format_tree(self) -> str:
         """Return formatted string with list of all files."""
         return '\n'.join(natural_sorted(self.files()))
 
-    def close(self):
+    def close(self) -> None:
         """Close file handle."""
         self._fh.close()
 
-    def __enter__(self):
+    def __enter__(self) -> CompoundFile:
         return self
 
     def __exit__(self, exc_type, exc_value, traceback):
         self.close()
 
-    def __str__(self):
-        """Return string with information about CompoundFile."""
-        s = [
-            self.__class__.__name__,
-            os.path.normpath(os.path.normcase(self.filename)),
-        ]
-        for attr in (
-            'clsid',
-            'version_minor',
-            'version_major',
-            'byteorder',
-            'dir_len',
-            'fat_len',
-            'dir_start',
-            'mini_stream_cutof_size',
-            'minifat_start',
-            'minifat_len',
-            'difat_start',
-            'difat_len',
-        ):
-            s.append(f'{attr}: {getattr(self, attr)}')
-        return '\n '.join(s)
+    def __repr__(self) -> str:
+        filename = os.path.split(self.filename)[-1]
+        return f'<{self.__class__.__name__} {filename!r}>'
+
+    def __str__(self) -> str:
+        return indent(
+            repr(self),
+            *(
+                f'{attr}: {getattr(self, attr)}'
+                for attr in (
+                    'clsid',
+                    'version_minor',
+                    'version_major',
+                    'byteorder',
+                    'dir_len',
+                    'fat_len',
+                    'dir_start',
+                    'mini_stream_cutof_size',
+                    'minifat_start',
+                    'minifat_len',
+                    'difat_start',
+                    'difat_len',
+                )
+            ),
+        )
 
 
 class DirectoryEntry:
-    """Compound Document Directory Entry."""
+    """Compound Document Directory Entry.
+
+    Parameters:
+        header:
+            128 bytes compound document directory entry header.
+        version_major:
+            Major version of compound document.
+
+    """
 
     __slots__ = (
         'name',
@@ -823,8 +1030,22 @@
         'is_storage',
     )
 
-    def __init__(self, data, version_major):
-        """Initialize directory entry from 128 bytes."""
+    name: str
+    entry_type: int
+    color: int
+    left_sibling_id: int
+    right_sibling_id: int
+    child_id: int
+    clsid: bytes | None
+    user_flags: int
+    create_time: datetime | None
+    modify_time: datetime | None
+    sector_start: int
+    stream_size: int
+    is_stream: bool
+    is_storage: bool
+
+    def __init__(self, header: bytes, version_major: int, /) -> None:
         (
             name,
             name_len,
@@ -839,10 +1060,10 @@
             modify_time,
             self.sector_start,
             self.stream_size,
-        ) = struct.unpack('<64sHBBIII16sIQQIQ', data)
+        ) = struct.unpack('<64sHBBIII16sIQQIQ', header)
 
         if version_major == 3:
-            self.stream_size = struct.unpack('<I', data[-8:-4])[0]
+            self.stream_size = struct.unpack('<I', header[-8:-4])[0]
         if self.clsid == b'\000' * 16:
             self.clsid = None
 
@@ -857,23 +1078,33 @@
         self.is_stream = self.entry_type == 2
         self.is_storage = self.entry_type == 1
 
-    def __str__(self):
-        """Return string with information about DirectoryEntry."""
-        s = [self.__class__.__name__]
-        for attr in self.__slots__[1:]:
-            s.append(f'{attr}: {getattr(self, attr)}')
-        return '\n '.join(s)
+    def __repr__(self) -> str:
+        return f'<{self.__class__.__name__} {self.name!r}>'
+
+    def __str__(self) -> str:
+        return indent(
+            repr(self),
+            *(f'{attr}: {getattr(self, attr)}' for attr in self.__slots__[1:]),
+        )
+
+
+def indent(*args) -> str:
+    """Return joined string representations of objects with indented lines."""
+    text = '\n'.join(str(arg) for arg in args)
+    return '\n'.join(
+        ('  ' + line if line else line) for line in text.splitlines() if line
+    )[2:]
 
 
 def format_dict(
-    adict,
-    prefix=' ',
-    indent=' ',
-    bullets=('', ''),
-    excludes=('_',),
-    linelen=79,
-    trim=1,
-):
+    adict: dict[str, Any],
+    prefix: str = ' ',
+    indent: str = ' ',
+    bullets: tuple[str, str] = ('', ''),
+    excludes: tuple[str] = ('_',),
+    linelen: int = 79,
+    trim: int = 1,
+) -> str:
     """Return pretty-print of nested dictionary."""
     result = []
     for k, v in sorted(adict.items(), key=lambda x: str(x[0]).lower()):
@@ -891,31 +1122,84 @@
     return '\n'.join(result)
 
 
-def astype(value, types=None):
-    """Return argument as one of types if possible."""
-    if value[0] in '\'"':
-        return value[1:-1]
+def astype(arg: str, types: Iterable[type] | None = None) -> Any:
+    """Return argument as one of types if possible.
+
+    Parameters:
+        arg:
+            String representation of value.
+        types:
+            Possible types of value. By default, int, float, and str.
+
+    """
+    if arg[0] in '\'"':
+        return arg[1:-1]
     if types is None:
         types = int, float, str
     for typ in types:
         try:
-            return typ(value)
+            return typ(arg)
         except (ValueError, TypeError, UnicodeEncodeError):
             pass
-    return value
+    return arg
+
+
+def filetime(ft: int, /) -> datetime | None:
+    """Return Python datetime from Microsoft FILETIME number.
 
+    Parameters:
+        ft: Microsoft FILETIME number.
 
-def filetime(ft):
-    """Return Python datetime from Microsoft FILETIME number."""
+    """
     if not ft:
         return None
     sec, nsec = divmod(ft - 116444736000000000, 10000000)
     return datetime.utcfromtimestamp(sec).replace(microsecond=(nsec // 10))
 
 
-if __name__ == '__main__':
-    import doctest
-    import tempfile  # noqa: used in doctrings
+def main(argv: list[str] | None = None) -> int:
+    """Oiffile command line usage main function.
+
+    ``python -m oiffile file_or_directory``
+
+    """
+    if argv is None:
+        argv = sys.argv
+
+    if len(argv) > 1 and '--test' in argv:
+        import doctest
+
+        m: Any
+        try:
+            import oiffile.oiffile
 
-    numpy.set_printoptions(suppress=True, precision=5)
-    doctest.testmod(optionflags=doctest.ELLIPSIS)
+            m = oiffile.oiffile
+        except ImportError:
+            m = None
+        print('running doctests')
+        numpy.set_printoptions(suppress=True, precision=5)
+        doctest.testmod(m, optionflags=doctest.ELLIPSIS)
+        return 0
+
+    if len(argv) != 2:
+        print('Usage: python -m oiffile file_or_directory')
+        return 0
+
+    from tifffile import imshow
+    from matplotlib import pyplot
+
+    with OifFile(sys.argv[1]) as oif:
+        print(oif)
+        print(oif.mainfile)
+        for series in oif.series:
+            # print(series)
+            image = series.asarray()
+            figure = pyplot.figure()
+            imshow(image, figure=figure)
+        pyplot.show()
+
+    return 0
+
+
+if __name__ == '__main__':
+    sys.exit(main())
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/oiffile-2020.9.18/setup.py 
new/oiffile-2022.9.29/setup.py
--- old/oiffile-2020.9.18/setup.py      2020-09-19 06:59:59.000000000 +0200
+++ new/oiffile-2022.9.29/setup.py      2022-09-30 07:53:32.000000000 +0200
@@ -7,28 +7,37 @@
 
 from setuptools import setup
 
+
+def search(pattern, code, flags=0):
+    # return first match for pattern in code
+    match = re.search(pattern, code, flags)
+    if match is None:
+        raise ValueError(f'{pattern!r} not found')
+    return match.groups()[0]
+
+
 with open('oiffile/oiffile.py') as fh:
     code = fh.read()
 
-version = re.search(r"__version__ = '(.*?)'", code).groups()[0]
+version = search(r"__version__ = '(.*?)'", code)
 
-description = re.search(r'"""(.*)\.(?:\r\n|\r|\n)', code).groups()[0]
+description = search(r'"""(.*)\.(?:\r\n|\r|\n)', code)
 
-readme = re.search(
-    r'(?:\r\n|\r|\n){2}"""(.*)"""(?:\r\n|\r|\n){2}__version__',
+readme = search(
+    r'(?:\r\n|\r|\n){2}"""(.*)"""(?:\r\n|\r|\n){2}[__version__|from]',
     code,
     re.MULTILINE | re.DOTALL,
-).groups()[0]
+)
 
 readme = '\n'.join(
     [description, '=' * len(description)] + readme.splitlines()[1:]
 )
 
-license = re.search(
+license = search(
     r'(# Copyright.*?(?:\r\n|\r|\n))(?:\r\n|\r|\n)+""',
     code,
     re.MULTILINE | re.DOTALL,
-).groups()[0]
+)
 
 license = license.replace('# ', '').replace('#', '')
 
@@ -42,20 +51,20 @@
 setup(
     name='oiffile',
     version=version,
+    license='BSD',
     description=description,
     long_description=readme,
     author='Christoph Gohlke',
-    author_email='cgoh...@uci.edu',
-    url='https://www.lfd.uci.edu/~gohlke/',
+    author_email='cgoh...@cgohlke.com',
+    url='https://www.cgohlke.com',
     project_urls={
         'Bug Tracker': 'https://github.com/cgohlke/oiffile/issues',
         'Source Code': 'https://github.com/cgohlke/oiffile',
         # 'Documentation': 'https://',
     },
-    license='BSD',
     packages=['oiffile'],
-    python_requires='>=3.6',
-    install_requires=['numpy>=1.15.1', 'tifffile>=2020.6.3'],
+    python_requires='>=3.8',
+    install_requires=['numpy>=1.19.2', 'tifffile>=2021.11.2'],
     platforms=['any'],
     classifiers=[
         'Development Status :: 4 - Beta',
@@ -64,8 +73,9 @@
         'Intended Audience :: Developers',
         'Operating System :: OS Independent',
         'Programming Language :: Python :: 3 :: Only',
-        'Programming Language :: Python :: 3.7',
         'Programming Language :: Python :: 3.8',
         'Programming Language :: Python :: 3.9',
+        'Programming Language :: Python :: 3.10',
+        'Programming Language :: Python :: 3.11',
     ],
 )

Reply via email to