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', ], )