Script 'mail_helper' called by obssrc
Hello community,
here is the log from the commit of package python-cmapfile for openSUSE:Factory
checked in at 2023-01-28 18:43:03
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Comparing /work/SRC/openSUSE:Factory/python-cmapfile (Old)
and /work/SRC/openSUSE:Factory/.python-cmapfile.new.32243 (New)
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Package is "python-cmapfile"
Sat Jan 28 18:43:03 2023 rev:3 rq:1061495 version:2022.9.29
Changes:
--------
--- /work/SRC/openSUSE:Factory/python-cmapfile/python-cmapfile.changes
2021-02-15 23:20:12.583746148 +0100
+++
/work/SRC/openSUSE:Factory/.python-cmapfile.new.32243/python-cmapfile.changes
2023-01-28 19:00:56.763929500 +0100
@@ -1,0 +2,19 @@
+Thu Jan 26 22:55:59 UTC 2023 - Ben Greiner <[email protected]>
+
+- Clean specfile
+- Unify buildtime and runtime requirements: Use the setup.py
+ specification, not the loose recommendation from the README
+
+-------------------------------------------------------------------
+Fri Jan 13 09:42:39 UTC 2023 - Dirk Müller <[email protected]>
+
+- update to 2022.9.29:
+ * Make subsampling compatible with ChimeraX (breaking).
+ * Fix deprecated import of scipy.ndimage.interpolation.zoom.
+ * Switch to Google style docstrings.
+ * Add type hints.
+ * Drop support for Python 3.7 and numpy < 1.19 (NEP29).
+ * Fix LSM conversion with tifffile >= 2021.2.26.
+ * Remove support for Python 3.6 (NEP 29).
+
+-------------------------------------------------------------------
Old:
----
cmapfile-2020.1.1.tar.gz
New:
----
cmapfile-2022.9.29.tar.gz
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Other differences:
------------------
++++++ python-cmapfile.spec ++++++
--- /var/tmp/diff_new_pack.ZMPQxp/_old 2023-01-28 19:00:57.163931706 +0100
+++ /var/tmp/diff_new_pack.ZMPQxp/_new 2023-01-28 19:00:57.167931728 +0100
@@ -1,7 +1,7 @@
#
# spec file for package python-cmapfile
#
-# 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,33 +16,33 @@
#
-%define skip_python2 1
-# NEP 29: TW python36-numpy -scipy and -tifffile are no more
-%define skip_python36 1
-%define packagename cmapfile
-%{?!python_module:%define python_module() python-%{**} python3-%{**}}
Name: python-cmapfile
-Version: 2020.1.1
+Version: 2022.9.29
Release: 0
Summary: Write Chimera Map (CMAP) files
License: BSD-3-Clause
Group: Development/Languages/Python
URL: https://www.lfd.uci.edu/~gohlke/
-Source:
https://github.com/cgohlke/cmapfile/archive/v%{version}.tar.gz#/%{packagename}-%{version}.tar.gz
-BuildRequires: %{python_module h5py >= 2.9}
-BuildRequires: %{python_module numpy >= 1.14.5}
-BuildRequires: %{python_module scipy >= 1.2}
+# SourceRepository: https://github.com/cgohlke/cmapfile
+Source:
https://github.com/cgohlke/cmapfile/archive/v%{version}.tar.gz#/cmapfile-%{version}.tar.gz
+BuildRequires: %{python_module base >= 3.8}
+BuildRequires: %{python_module h5py >= 3.1}
+BuildRequires: %{python_module numpy >= 1.19.2}
+BuildRequires: %{python_module oiffile >= 2021.6.6}
+BuildRequires: %{python_module pip}
+BuildRequires: %{python_module scipy >= 1.5}
BuildRequires: %{python_module setuptools}
-BuildRequires: %{python_module tifffile >= 2019.1.1}
+BuildRequires: %{python_module tifffile >= 2021.11.2}
+BuildRequires: %{python_module wheel}
BuildRequires: fdupes
BuildRequires: python-rpm-macros
-Requires: python-h5py >= 2.9
-Requires: python-numpy >= 1.14.5
-Requires: python-oiffile >= 2020.1.1
-Requires: python-scipy >= 1.2
-Requires: python-tifffile >= 2019.1.1
+Requires: python-h5py >= 3.1
+Requires: python-numpy >= 1.19.2
+Requires: python-oiffile >= 2021.6.6
+Requires: python-scipy >= 1.5
+Requires: python-tifffile >= 2021.11.2
Requires(post): update-alternatives
-Requires(postun): update-alternatives
+Requires(postun):update-alternatives
BuildArch: noarch
%python_subpackages
@@ -50,36 +50,32 @@
Create Chimera MAP files from various file formats containing volume data.
%prep
-%setup -q -n %{packagename}-%{version}
+%setup -q -n cmapfile-%{version}
# Fix warning wrong-file-end-of-line-encoding
sed -i 's/\r//' README.rst
%build
-%python_build
+%pyproject_wheel
%install
-%python_install
-for p in %{packagename} ; do
- %python_clone -a %{buildroot}%{_bindir}/$p
-done
-
+%pyproject_install
+%python_clone -a %{buildroot}%{_bindir}/cmapfile
%python_expand %fdupes %{buildroot}%{$python_sitelib}
-%prepare_alternative %{packagename}
%post
-%python_install_alternative %{packagename}
+%python_install_alternative cmapfile
%postun
-%python_uninstall_alternative %{packagename}
+%python_uninstall_alternative cmapfile
%check
-# Test not provided
+# No tests by upstream
%files %{python_files}
%doc README.rst
%license LICENSE
-%python_alternative %{_bindir}/%{packagename}
-%{python_sitelib}/*egg-info/
-%{python_sitelib}/%{packagename}/
+%python_alternative %{_bindir}/cmapfile
+%{python_sitelib}/cmapfile
+%{python_sitelib}/cmapfile-%{version}.dist-info
%changelog
++++++ cmapfile-2020.1.1.tar.gz -> cmapfile-2022.9.29.tar.gz ++++++
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/cmapfile-2020.1.1/LICENSE
new/cmapfile-2022.9.29/LICENSE
--- old/cmapfile-2020.1.1/LICENSE 2020-01-18 10:05:10.000000000 +0100
+++ new/cmapfile-2022.9.29/LICENSE 2022-09-30 06:20:32.000000000 +0200
@@ -1,6 +1,6 @@
BSD 3-Clause License
-Copyright (c) 2014-2020, Christoph Gohlke
+Copyright (c) 2014-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/cmapfile-2020.1.1/README.rst
new/cmapfile-2022.9.29/README.rst
--- old/cmapfile-2020.1.1/README.rst 2020-01-18 10:05:10.000000000 +0100
+++ new/cmapfile-2022.9.29/README.rst 2022-09-30 06:20:32.000000000 +0200
@@ -5,35 +5,32 @@
files, HDF5 files containing series of 3D XYZ datasets.
CMAP files can be created from numpy arrays and various file formats
-containing volume data, e.g. BIN, TIFF, LSM, OIF, and OIB.
+containing volume data, e.g., BIN, TIFF, LSM, OIF, and OIB.
-CMAP files can be visualized using UCSF Chimera [2], a highly extensible
-program for interactive visualization and analysis of molecular structures
-and related data.
-
-For command line usage run ``python -m cmapfile --help``
-
-:Author:
- `Christoph Gohlke <https://www.lfd.uci.edu/~gohlke/>`_
-
-:Organization:
- Laboratory for Fluorescence Dynamics. University of California, Irvine
+CMAP files can be visualized using UCSF Chimera [2], a program for interactive
+visualization and analysis of molecular structures and related data.
+:Author: `Christoph Gohlke <https://www.cgohlke.com>`_
:License: BSD 3-Clause
-
-:Version: 2020.1.1
+:Version: 2022.9.29
Requirements
------------
-* `CPython >= 3.6 <https://www.python.org>`_
-* `Numpy 1.14 <https://www.numpy.org>`_
-* `Scipy 1.1 <https://www.scipy.org>`_
-* `H5py 2.10 <https://www.h5py.org/>`_
-* `Tifffile 2019.1.1 <https://pypi.org/project/tifffile/>`_
-* `Oiffile 2020.1.1 <https://pypi.org/project/oiffile/>`_
+
+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>`_
+ (32-bit platforms are deprecated)
+- `Numpy 1.21.5 <https://pypi.org/project/numpy/>`_
+- `Scipy 1.8.1 <https://pypi.org/project/scipy/>`_
+- `H5py 3.7.0 <https://pypi.org/project/h5py/>`_
+- `Tifffile 2022.8.12 <https://pypi.org/project/tifffile/>`_ (optional)
+- `Oiffile 2022.2.2 <https://pypi.org/project/oiffile />`_ (optional)
References
----------
+
1. Thomas Goddard. [Chimera-users] reading in hdf5 files in chimera.
https://www.cgl.ucsf.edu/pipermail/chimera-users/2008-September/003052.html
2. UCSF Chimera, an extensible molecular modeling system.
@@ -42,12 +39,17 @@
Examples
--------
+
+Print the command line usage::
+
+ python -m cmapfile --help
+
Convert a 5D LSM file to CMAP file::
python -m cmapfile "/my data directory/large.lsm"
Convert all BIN files in the current directory to test.cmap. The BIN files
-are known to contain 128x128x64 samples of 16 bit integers. The CMAP file
+are known to contain 128x128x64 samples of 16-bit integers. The CMAP file
will store float32 maps using subsampling up to 16::
python -m cmapfile --shape 128,128,64 --step 1,1,2 --dtype i2
@@ -59,6 +61,7 @@
Notes
-----
+
The CMAP file format according to [1]::
Example of HDF format written by Chimera (Chimera map format) follows.
@@ -72,20 +75,44 @@
cell_angles (90.0, 90.0, 90.0) (attribute)
rotation_axis (0.0, 0.0, 1.0) (attribute)
rotation_angle 45.0 (attribute, degrees)
+ color (1.0, 1.0, 0, 1.0) (attribute, rgba 0-1 float)
+ time 5 (attribute, time series frame number)
+ channel 0 (attribute, integer for multichannel data)
/data (3d array of uint8 (123,542,82)) (dataset, any name allowed)
/data_x (3d array of uint8 (123,542,82), alternate chunk shape) (dataset)
/data_2 (3d array of uint8 (61,271,41)) (dataset, any name allowed)
subsample_spacing (2, 2, 2) (attribute)
(more subsampled or alternate chunkshape versions of same data)
-
Revisions
---------
+
+2022.9.29
+
+- Make subsampling compatible with ChimeraX (breaking).
+- Fix deprecated import of scipy.ndimage.interpolation.zoom.
+- Switch to Google style docstrings.
+
+2022.2.2
+
+- Add type hints.
+- Drop support for Python 3.7 and numpy < 1.19 (NEP29).
+
+2021.2.26
+
+- Fix LSM conversion with tifffile >= 2021.2.26.
+- Remove support for Python 3.6 (NEP 29).
+
2020.1.1
- Do not write name attribute.
- Remove support for Python 2.7 and 3.5.
- Update copyright.
+
+- Do not write name attribute.
+- Remove support for Python 2.7 and 3.5.
+- Update copyright.
+
2018.8.30
- Move cmapfile.py into cmapfile package.
+
+- Move cmapfile.py into cmapfile package.
+
2014.10.10
- Initial release.
+
+- Initial release.
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/cmapfile-2020.1.1/cmapfile/cmapfile.py
new/cmapfile-2022.9.29/cmapfile/cmapfile.py
--- old/cmapfile-2020.1.1/cmapfile/cmapfile.py 2020-01-18 10:05:10.000000000
+0100
+++ new/cmapfile-2022.9.29/cmapfile/cmapfile.py 2022-09-30 06:20:32.000000000
+0200
@@ -1,6 +1,6 @@
# cmapfile.py
-# Copyright (c) 2014-2020, Christoph Gohlke
+# Copyright (c) 2014-2022, Christoph Gohlke
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
@@ -35,35 +35,32 @@
files, HDF5 files containing series of 3D XYZ datasets.
CMAP files can be created from numpy arrays and various file formats
-containing volume data, e.g. BIN, TIFF, LSM, OIF, and OIB.
+containing volume data, e.g., BIN, TIFF, LSM, OIF, and OIB.
-CMAP files can be visualized using UCSF Chimera [2], a highly extensible
-program for interactive visualization and analysis of molecular structures
-and related data.
-
-For command line usage run ``python -m cmapfile --help``
-
-:Author:
- `Christoph Gohlke <https://www.lfd.uci.edu/~gohlke/>`_
-
-:Organization:
- Laboratory for Fluorescence Dynamics. University of California, Irvine
+CMAP files can be visualized using UCSF Chimera [2], a program for interactive
+visualization and analysis of molecular structures and related data.
+:Author: `Christoph Gohlke <https://www.cgohlke.com>`_
:License: BSD 3-Clause
-
-:Version: 2020.1.1
+:Version: 2022.9.29
Requirements
------------
-* `CPython >= 3.6 <https://www.python.org>`_
-* `Numpy 1.14 <https://www.numpy.org>`_
-* `Scipy 1.1 <https://www.scipy.org>`_
-* `H5py 2.10 <https://www.h5py.org/>`_
-* `Tifffile 2019.1.1 <https://pypi.org/project/tifffile/>`_
-* `Oiffile 2020.1.1 <https://pypi.org/project/oiffile/>`_
+
+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>`_
+ (32-bit platforms are deprecated)
+- `Numpy 1.21.5 <https://pypi.org/project/numpy/>`_
+- `Scipy 1.8.1 <https://pypi.org/project/scipy/>`_
+- `H5py 3.7.0 <https://pypi.org/project/h5py/>`_
+- `Tifffile 2022.8.12 <https://pypi.org/project/tifffile/>`_ (optional)
+- `Oiffile 2022.2.2 <https://pypi.org/project/oiffile />`_ (optional)
References
----------
+
1. Thomas Goddard. [Chimera-users] reading in hdf5 files in chimera.
https://www.cgl.ucsf.edu/pipermail/chimera-users/2008-September/003052.html
2. UCSF Chimera, an extensible molecular modeling system.
@@ -72,12 +69,17 @@
Examples
--------
+
+Print the command line usage::
+
+ python -m cmapfile --help
+
Convert a 5D LSM file to CMAP file::
python -m cmapfile "/my data directory/large.lsm"
Convert all BIN files in the current directory to test.cmap. The BIN files
-are known to contain 128x128x64 samples of 16 bit integers. The CMAP file
+are known to contain 128x128x64 samples of 16-bit integers. The CMAP file
will store float32 maps using subsampling up to 16::
python -m cmapfile --shape 128,128,64 --step 1,1,2 --dtype i2
@@ -89,6 +91,7 @@
Notes
-----
+
The CMAP file format according to [1]::
Example of HDF format written by Chimera (Chimera map format) follows.
@@ -102,31 +105,62 @@
cell_angles (90.0, 90.0, 90.0) (attribute)
rotation_axis (0.0, 0.0, 1.0) (attribute)
rotation_angle 45.0 (attribute, degrees)
+ color (1.0, 1.0, 0, 1.0) (attribute, rgba 0-1 float)
+ time 5 (attribute, time series frame number)
+ channel 0 (attribute, integer for multichannel data)
/data (3d array of uint8 (123,542,82)) (dataset, any name allowed)
/data_x (3d array of uint8 (123,542,82), alternate chunk shape) (dataset)
/data_2 (3d array of uint8 (61,271,41)) (dataset, any name allowed)
subsample_spacing (2, 2, 2) (attribute)
(more subsampled or alternate chunkshape versions of same data)
-
Revisions
---------
+
+2022.9.29
+
+- Make subsampling compatible with ChimeraX (breaking).
+- Fix deprecated import of scipy.ndimage.interpolation.zoom.
+- Switch to Google style docstrings.
+
+2022.2.2
+
+- Add type hints.
+- Drop support for Python 3.7 and numpy < 1.19 (NEP29).
+
+2021.2.26
+
+- Fix LSM conversion with tifffile >= 2021.2.26.
+- Remove support for Python 3.6 (NEP 29).
+
2020.1.1
- Do not write name attribute.
- Remove support for Python 2.7 and 3.5.
- Update copyright.
+
+- Do not write name attribute.
+- Remove support for Python 2.7 and 3.5.
+- Update copyright.
+
2018.8.30
- Move cmapfile.py into cmapfile package.
+
+- Move cmapfile.py into cmapfile package.
+
2014.10.10
- Initial release.
+
+- Initial release.
"""
-__version__ = '2020.1.1'
+from __future__ import annotations
+
+__version__ = '2022.9.29'
-__all__ = (
- 'CmapFile', 'bin2cmap', 'tif2cmap', 'lsm2cmap', 'oif2cmap', 'array2cmap'
-)
+__all__ = [
+ 'CmapFile',
+ 'bin2cmap',
+ 'tif2cmap',
+ 'lsm2cmap',
+ 'oif2cmap',
+ 'array2cmap',
+]
import sys
import os
@@ -137,75 +171,120 @@
import h5py
try:
- from ndimage.interpolation import zoom
+ from scipy.ndimage import zoom
except ImportError:
- from scipy.ndimage.interpolation import zoom
+ from ndimage import zoom
from tifffile import TiffFile, transpose_axes, natural_sorted, product
from oiffile import OifFile
+from typing import TYPE_CHECKING
+
+if TYPE_CHECKING:
+ from typing import Any, Union, Sequence, Iterator
+
+ try:
+ from numpy.typing import ArrayLike
+ except ImportError:
+ # numpy < 1.20
+ from numpy import ndarray as ArrayLike
+
+ PathLike = Union[str, os.PathLike]
+
class CmapFile(h5py.File):
- """Write Chimera MAP formatted HDF5 file."""
+ """Write Chimera MAP formatted HDF5 file.
- def __init__(self, filename, mode='w', **kwargs):
- """Create new HDF5 file object.
+ Parameters:
+ filename:
+ Name of file to write.
+ mode:
+ File open mode.
+ **kwargs:
+ Arguments passed to `h5py.File`.
- See h5py.File for parameters.
+ """
- """
+ mapcounter: int
+
+ def __init__(
+ self, filename: PathLike, /, mode: str = 'w', **kwargs
+ ) -> None:
h5py.File.__init__(self, name=filename, mode=mode, **kwargs)
self.mapcounter = 0
- def addmap(self, data, name=None, step=None, origin=None,
- cell_angles=None, rotation_axis=None, rotation_angle=None,
- symmetries=None, astype=None, subsample=16, chunks=True,
- compression=None, verbose=False):
+ def addmap(
+ self,
+ data: ArrayLike,
+ /,
+ *,
+ name: str | None = None,
+ step: Sequence[float] | None = None,
+ origin: Sequence[float] | None = None,
+ cell_angles: Sequence[float] | None = None,
+ rotation_axis: Sequence[float] | None = None,
+ rotation_angle: float | None = None,
+ color: Sequence[float] | None = None,
+ time: int | None = None,
+ channel: int | None = None,
+ symmetries=None,
+ astype: numpy.dtype = None,
+ subsample: int = 16,
+ chunks: bool = True,
+ compression: str | None = None,
+ verbose: bool = False,
+ ) -> None:
"""Create HDF5 group and datasets according to CMAP format.
- The order of axes is XYZ for 'step', 'origin', 'cell_angles', and
- 'rotation_axis'. Data 'shape' and 'chunks' sizes are in ZYX order.
+ The order of axes is XYZ for `step`, `origin`, `cell_angles`, and
+ `rotation_axis`. Data shape and `chunks` sizes are in ZYX order.
- Parameters
- ----------
- data : array_like
- Map data to store. Must be three dimensional.
- name : str, optional
- Name of map.
- step : sequence of 3 float, optional
- Spacing between samples in XYZ dimensions.
- Chimera defaults to (1.0, 1.0, 1.0)
- origin : sequence of 3 float, optional
- Chimera defaults to (0.0, 0.0, 0.0)
- cell_angles : sequence of 3 float, optional
- Chimera defaults to (90.0, 90.0, 90.0)
- rotation_axis : sequence of 3 float, optional
- Axis to rotate around. Chimera defaults to (0.0, 0.0, 1.0)
- rotation_angle : float, optional
- Extent to rotate around rotation_axis. Chimera defaults to 0.0.
- symmetries : None, optional
- Undocumented.
- astype : numpy dtype, optional
- Datatype of HDF dataset, e.g. 'float32'.
- By default this is data.dtype.
- subsample : int, optional
- Store subsampled datasets up to the specified number (default: 16).
- chunks : bool or sequence of 3 int, optional
- Size of chunks to store in datasets in ZYX order.
- By default HDF5 determines this.
- compression : str, optional
- Type of HDF5 data compression, e.g. None (default) or 'gzip'.
- verbose : bool, optional
- If False (default), do not print messages to stdout.
+ Parameters:
+ data:
+ Map data to store. Must be three dimensional.
+ name:
+ Name of map.
+ step:
+ Spacing between samples in XYZ dimensions.
+ Chimera defaults to (1.0, 1.0, 1.0).
+ origin:
+ Origin in XYZ dimensions.
+ Chimera defaults to (0.0, 0.0, 0.0).
+ cell_angles:
+ Chimera defaults to (90.0, 90.0, 90.0).
+ rotation_axis:
+ Axis to rotate around. Chimera defaults to (0.0, 0.0, 1.0).
+ rotation_angle:
+ Extent to rotate around rotation_axis. Chimera defaults to 0.0.
+ color:
+ RGBA color values, e.g. (1.0, 1.0, 0, 1.0).
+ time:
+ Time series frame number.
+ channel:
+ Multi-channel index.
+ symmetries:
+ Undocumented.
+ astype:
+ Datatype of HDF dataset, e.g. 'float32'.
+ By default, this is data.dtype.
+ subsample:
+ Store subsampled datasets up to specified number.
+ chunks:
+ Size of chunks to store in datasets in ZYX order.
+ By default, the chunk size is determined by HDF5.
+ compression:
+ Type of HDF5 data compression, e.g. None or 'gzip'.
+ verbose:
+ Print messages to stdout.
"""
- data = numpy.atleast_3d(data)
- if data.ndim != 3:
- raise ValueError('map data must be 3 dimensional')
if astype:
data = numpy.ascontiguousarray(data, astype)
else:
data = numpy.ascontiguousarray(data)
+ if data.ndim != 3:
+ raise ValueError('map data must be 3 dimensional')
+
# create group and write attributes
group = self.create_group(f'map{self.mapcounter:05d}')
@@ -226,32 +305,42 @@
group.attrs['rotation_axis'] = rotation_axis
if rotation_angle:
group.attrs['rotation_angle'] = rotation_angle
+ if color:
+ group.attrs['color'] = color
+ if time:
+ group.attrs['time'] = time
+ if channel:
+ group.attrs['channel'] = channel
if symmetries:
group.attrs['symmetries'] = symmetries
# create main dataset
if verbose:
print('1 ', end='', flush=True)
- dset = group.create_dataset(f'data{self.mapcounter:05d}',
- data=data, chunks=chunks,
- compression=compression)
+ dset = group.create_dataset(
+ f'data{self.mapcounter:05d}',
+ data=data,
+ chunks=chunks,
+ compression=compression,
+ )
# create subsampled datasets
for i, data in enumerate(subsamples(data, int(subsample))):
- sample = 2**(i + 1)
+ sample = 2 ** (i + 1)
if verbose:
print(f'{sample} ', end='', flush=True)
dset = group.create_dataset(
f'data{self.mapcounter:05d}_{i + 2}',
- data=data, chunks=chunks, compression=compression)
+ data=data,
+ chunks=chunks,
+ compression=compression,
+ )
dset.attrs['subsample_spacing'] = sample, sample, sample
self.mapcounter += 1
- def setstep(self, step):
- """Set 'step' attribute on all datasets.
+ def setstep(self, step: Sequence[float], /) -> None:
+ """Set `step` attribute on all datasets.
- Parameters
- ----------
- step : sequence of 3 float
- Spacing between samples in XYZ dimensions.
+ Parameters:
+ step: Spacing between samples in XYZ dimensions.
"""
for name, group in self.items():
@@ -259,38 +348,43 @@
group.attrs.modify('step', step)
-def bin2cmap(binfiles, shape, dtype, offset=0, cmapfile=None, fail=True,
- **kwargs):
+def bin2cmap(
+ binfiles: Sequence[PathLike] | str,
+ /,
+ shape: tuple[int, ...],
+ dtype: numpy.dtype,
+ offset: int = 0,
+ cmapfile: PathLike | None = None,
+ fail: bool = True,
+ **kwargs,
+) -> None:
r"""Convert series of SimFCS BIN files to Chimera MAP file.
SimFCS BIN files contain homogeneous data of any type and shape,
stored C-contiguously in little endian order.
- A common format is: shape=(-1, 256, 256), dtype='uint16'.
+ A common format is `shape=(-1, 256, 256), dtype='uint16'`.
TODO: Support generic strides, storage order, and byteorder.
- Parameters
- ----------
- binfiles : str or sequence of str
- List of BIN file names or file pattern, e.g. '\*.bin'
- shape : tuple of 3 int
- Shape of data in BIN files in ZYX order, e.g. (32, 256, 256).
- dtype : numpy dtype
- Type of data in the BIN files, e.g. 'uint16'.
- offset : int, optional
- Number of bytes to skip at beginning of BIN file (default: 0).
- cmapfile : str, optional
- Name of the output CMAP file. If None (default), the name is
- derived from the first BIN file.
- fail : bool, optional
- If True (default), raise error when reading invalid BIN files.
- kwargs : dict, optional
- Additional parameters passed to the CmapFile.addmap function,
- e.g. verbose, step, origin, cell_angles, rotation_axis,
- rotation_angle, subsample, chunks, and compression.
+ Parameters:
+ binfiles:
+ List of BIN file names or file pattern, e.g. '\*.bin'
+ shape:
+ Shape of data in BIN files in ZYX order, e.g. (32, 256, 256).
+ dtype:
+ Type of data in BIN files, e.g. 'uint16'.
+ offset:
+ Number of bytes to skip at beginning of BIN file.
+ cmapfile:
+ Name of output CMAP file.
+ If None, the name is derived from the first BIN file.
+ fail:
+ Raise error when reading invalid BIN files.
+ **kwargs:
+ Arguments passed to :py:meth:`CmapFile.addmap`.
"""
- binfiles = parse_files(binfiles)
+ binfiles_list = parse_files(binfiles)
validate_shape(shape, 3)
shape = tuple(shape)
dtype = numpy.dtype(dtype)
@@ -298,12 +392,12 @@
if count < 0:
count = -1
if not cmapfile:
- cmapfile = binfiles[0] + '.cmap'
+ cmapfile = os.fspath(binfiles_list[0]) + '.cmap'
verbose = kwargs.get('verbose', False)
if verbose:
print(f"Creating '{cmapfile}'", flush=True)
with CmapFile(cmapfile, 'w') as cmap:
- for binfile in binfiles:
+ for binfile in binfiles_list:
if verbose:
print('+', os.path.basename(binfile), end=' ', flush=True)
try:
@@ -323,34 +417,37 @@
print(flush=True)
-def tif2cmap(tiffiles, cmapfile=None, fail=True, **kwargs):
+def tif2cmap(
+ tiffiles: Sequence[PathLike],
+ /,
+ cmapfile: PathLike | None = None,
+ fail: bool = True,
+ **kwargs,
+) -> None:
r"""Convert series of 3D TIFF files to Chimera MAP file.
- Parameters
- ----------
- tiffiles : str or sequence of str
- List of TIFF file names or file pattern, e.g. '\*.tif'.
- Files must contain 3D data of matching shape and dtype.
- cmapfile : str, optional
- Name of the output CMAP file. If None (default), the name is
- derived from the first TIFF file.
- fail : bool, optional
- If True (default), raise error when processing incompatible TIFF files.
- kwargs : dict, optional
- Additional parameters passed to the CmapFile.addmap function,
- e.g. verbose, step, origin, cell_angles, rotation_axis,
- rotation_angle, subsample, chunks, and compression.
+ Parameters:
+ tiffiles:
+ TIFF file names or file pattern, e.g. '\*.tif'.
+ Files must contain 3D data of matching shape and dtype.
+ cmapfile:
+ Name of output CMAP file.
+ By default, the name is derived from the first TIFF file.
+ fail:
+ Raise error when processing incompatible TIFF files.
+ **kwargs:
+ Arguments passed to :py:meth:`CmapFile.addmap`.
"""
- tiffiles = parse_files(tiffiles)
+ tiffiles_list = parse_files(tiffiles)
if not cmapfile:
- cmapfile = tiffiles[0] + '.cmap'
+ cmapfile = os.fspath(tiffiles_list[0]) + '.cmap'
verbose = kwargs.get('verbose', False)
if verbose:
print(f"Creating '{cmapfile}'", flush=True)
shape = dtype = None
with CmapFile(cmapfile, 'w') as cmap:
- for tiffile in tiffiles:
+ for tiffile in tiffiles_list:
if verbose:
print('+', os.path.basename(tiffile), end=' ', flush=True)
try:
@@ -375,20 +472,19 @@
print(flush=True)
-def lsm2cmap(lsmfile, cmapfile=None, **kwargs):
+def lsm2cmap(
+ lsmfile: PathLike, /, cmapfile: PathLike | None = None, **kwargs
+) -> None:
"""Convert 5D TZCYX LSM file to Chimera MAP files, one per channel.
- Parameters
- ----------
- lsmfile : str
- Name of the LSM file to convert.
- cmapfile : str, optional
- Name of the output CMAP file. If None (default), the name is
- derived from lsmfile.
- kwargs : dict, optional
- Additional parameters passed to the CmapFile.addmap function,
- e.g. verbose, step, origin, cell_angles, rotation_axis,
- rotation_angle, subsample, chunks, and compression.
+ Parameters:
+ lsmfile:
+ Name of LSM file to convert.
+ cmapfile:
+ Name of output CMAP file.
+ If None, the name is derived from `lsmfile`.
+ **kwargs:
+ Arguments passed to :py:meth:`CmapFile.addmap`.
"""
verbose = kwargs.get('verbose', False)
@@ -398,19 +494,27 @@
# open LSM file
lsm = TiffFile(lsmfile)
series = lsm.series[0] # first series contains the image data
- if series.axes != 'TZCYX':
- raise ValueError(
- f'not a 5D LSM file (expected TZCYX, got {series.axes})'
- )
+ if hasattr(series, 'get_shape'):
+ # tifffile > 2020.2.25 return squeezed shape and axes
+ shape = series.get_shape(False)
+ axes = series.get_axes(False)
+ if axes[:2] == 'MP' and shape[:2] == (1, 1):
+ axes = axes[2:]
+ shape = shape[2:]
+ else:
+ shape = series.shape
+ axes = series.axes
+ if axes != 'TZCYX':
+ raise ValueError(f'not a 5D LSM file (expected TZCYX, got {axes})')
if verbose:
print(lsm)
- print(series.shape, series.axes, flush=True)
+ print(shape, axes, flush=True)
# create one CMAP file per channel
if cmapfile:
cmapfile = '{}.ch%04d{}'.format(*os.path.splitext(cmapfile))
else:
cmapfile = f'{lsmfile}.ch%04d.cmap'
- cmaps = [CmapFile(cmapfile % i) for i in range(series.shape[2])]
+ cmaps = [CmapFile(cmapfile % i) for i in range(shape[2])]
# voxel/step sizes
if not kwargs.get('step', None):
try:
@@ -418,19 +522,20 @@
kwargs['step'] = (
attrs['voxel_size_x'] / attrs['voxel_size_x'],
attrs['voxel_size_y'] / attrs['voxel_size_x'],
- attrs['voxel_size_z'] / attrs['voxel_size_x'])
+ attrs['voxel_size_z'] / attrs['voxel_size_x'],
+ )
except Exception:
pass
# iterate over Tiff pages containing data
pages = iter(series.pages)
- for _ in range(series.shape[0]): # iterate over time axis
- data = []
- for _ in range(series.shape[1]): # iterate over z slices
- data.append(next(pages).asarray())
- data = numpy.vstack(data).reshape(series.shape[1:])
- for c in range(series.shape[2]): # iterate over channels
+ for t in range(shape[0]): # iterate over time axis
+ datalist = []
+ for _ in range(shape[1]): # iterate over z slices
+ datalist.append(next(pages).asarray())
+ data = numpy.vstack(datalist).reshape(shape[1:])
+ for c in range(shape[2]): # iterate over channels
# write datasets and attributes
- cmaps[c].addmap(data=data[:, c], **kwargs)
+ cmaps[c].addmap(data[:, c], time=t, **kwargs)
finally:
if lsm:
lsm.close()
@@ -438,22 +543,21 @@
f.close()
-def array2cmap(data, axes, cmapfile, **kwargs):
+def array2cmap(
+ data: numpy.ndarray, /, axes: str, cmapfile: PathLike, **kwargs
+) -> None:
"""Save numpy ndarray to Chimera MAP files, one per channel.
- Parameters
- ----------
- data : ndarray
- Three to 5 dimensional array.
- axes : str
- Specifies type and order of axes in data array.
- May contain only 'CTZYX'.
- cmapfile : str
- Name of the output CMAP file.
- kwargs : dict, optional
- Additional parameters passed to the CmapFile.addmap function,
- e.g. verbose, step, origin, cell_angles, rotation_axis,
- rotation_angle, subsample, chunks, and compression.
+ Parameters:
+ data:
+ Three to 5 dimensional array.
+ axes:
+ Specifies type and order of axes in data array.
+ May contain only 'CTZYX'.
+ cmapfile:
+ Name of output CMAP file.
+ **kwargs:
+ Arguments passed to :py:meth:`CmapFile.addmap`.
"""
if len(data.shape) != len(axes):
@@ -462,47 +566,45 @@
try:
# create one CMAP file per channel
cmaps = []
+ cmapfile = os.fspath(cmapfile)
if cmapfile.lower().endswith('.cmap'):
cmapfile = cmapfile[:-5]
if data.shape[0] > 1:
- cmaps = [CmapFile(f'{cmapfile}.ch{i:04d}.cmap')
- for i in range(data.shape[0])]
+ cmaps = [
+ CmapFile(f'{cmapfile}.ch{i:04d}.cmap')
+ for i in range(data.shape[0])
+ ]
else:
cmaps = [CmapFile(f'{cmapfile}.cmap')]
# iterate over data and write cmaps
for c in range(data.shape[0]): # channels
for t in range(data.shape[1]): # times
- cmaps[c].addmap(data=data[c, t], **kwargs)
+ cmaps[c].addmap(data[c, t], time=t, **kwargs)
finally:
for f in cmaps:
f.close()
-def oif2cmap(oiffile, cmapfile=None, **kwargs):
+def oif2cmap(
+ oiffile: PathLike, /, cmapfile: PathLike = None, **kwargs
+) -> None:
"""Convert OIF or OIB files to Chimera MAP files, one per channel.
- Parameters
- ----------
- oiffile : str
- Name of the OIF or OIB file to convert.
- cmapfile : str, optional
- Name of the output CMAP file. If None (default), the name is
- derived from oiffile.
- kwargs : dict, optional
- Additional parameters passed to the CmapFile.addmap function,
- e.g. verbose, step, origin, cell_angles, rotation_axis,
- rotation_angle, subsample, chunks, and compression.
+ Parameters:
+ oiffile:
+ Name of OIF or OIB file to convert.
+ cmapfile:
+ Name of output CMAP file.
+ By default, the name is derived from `oiffile`.
+ **kwargs:
+ Arguments passed to :py:meth:`CmapFile.addmap`.
"""
verbose = kwargs.get('verbose', False)
with OifFile(oiffile) as oif:
if verbose:
print(oif)
- try:
- tiffs = oif.series[0]
- except Exception:
- # oiffile < 2020.1.1
- tiffs = oif.tiffs
+ tiffs = oif.series[0]
data = tiffs.asarray()
axes = tiffs.axes + 'YX'
if verbose:
@@ -516,7 +618,8 @@
kwargs['step'] = (
1.0,
(size['Y'] / (shape[-2] - 1)) / xsize,
- (size['Z'] / (shape[axes.index('Z')] - 1)) / xsize)
+ (size['Z'] / (shape[axes.index('Z')] - 1)) / xsize,
+ )
except Exception:
pass
if cmapfile is None:
@@ -524,8 +627,13 @@
array2cmap(data, axes, cmapfile, **kwargs)
-def oif_axis_size(oifsettings):
- """Return dict of axes sizes from OIF main settings."""
+def oif_axis_size(oifsettings: dict[str, Any], /) -> dict[str, Any]:
+ """Return mapping of axes sizes from OIF main settings.
+
+ Parameters:
+ oifsettings: OIF main settings.
+
+ """
scale = {'nm': 1000.0, 'ms': 1000.0}
result = {}
i = 0
@@ -541,18 +649,44 @@
return result
-def subsamples(data, maxsample=16, minshape=4):
- """Return iterator over data zoomed by 0.5."""
+def subsamples(
+ data: numpy.ndarray, /, maxsample: int = 16, minshape: int = 4
+) -> Iterator[numpy.ndarray]:
+ """Return iterator over data zoomed by ~0.5.
+
+ Parameters:
+ data:
+ Data to be resampled.
+ maxsample:
+ Inverse of maximum zoom factor.
+ minshape:
+ Minimum size of any dimension.
+
+ """
# TODO: use faster mipmap or gaussian pyramid generator
+ zoomed = data
+ zooms = [1.0 for size in data.shape]
sample = 2
- while sample <= maxsample and all(i >= minshape for i in data.shape):
- data = zoom(data, 0.5, prefilter=False)
+ while sample <= maxsample and all(i >= minshape for i in zoomed.shape):
+ # this formula is used by ChimeraX to calculate subsample zoom factors
+ zooms = [((i + sample - 1) // sample) / i for i in data.shape]
+ zoomed = zoom(data, zooms, prefilter=False)
+ yield zoomed
sample *= 2
- yield data
-def validate_shape(shape, length=None):
- """Raise ValueError if shape is not a sequence of positive integers."""
+def validate_shape(
+ shape: tuple[int, ...], /, length: int | None = None
+) -> None:
+ """Raise ValueError if shape is not a sequence of positive integers.
+
+ Parameters:
+ shape:
+ Shape to validate.
+ length:
+ Expected length of `shape`.
+
+ """
try:
if length is not None and len(shape) != length:
raise ValueError()
@@ -562,8 +696,20 @@
raise ValueError('invalid shape') from exc
-def parse_numbers(numbers, dtype=float, sep=','):
- """Return list of numbers from string of separated numbers."""
+def parse_numbers(
+ numbers: str, /, dtype: type = float, sep: str = ','
+) -> list[Any]:
+ """Return list of numbers from string of separated numbers.
+
+ Parameters:
+ numbers:
+ Numbers of type `dtype` separated by `sep.`
+ dtype:
+ Type of numbers.
+ sep:
+ Separator used to split numbers.
+
+ """
if not numbers:
return []
try:
@@ -572,10 +718,14 @@
raise ValueError(f"not a '{sep}' separated list of numbers") from exc
-def parse_files(files):
+def parse_files(files: Sequence[PathLike], /) -> Sequence[PathLike]:
"""Return list of file names from pattern or list of file names.
- Raise ValueError if no files are found.
+ Parameters:
+ files: Sequence of file names.
+
+ Raises:
+ ValueError: No files are found.
"""
# # list of files as string
@@ -583,13 +733,15 @@
# files = natural_sorted(
# match.group(1) or match.group(2)
# for match in re.finditer(r'(?:"([^"\t\n\r\f\v]+))"|(\S+)',
- # files))
- try: # list of files
- if os.path.isfile(files[0]):
+ try:
+ # list of files
+ if isinstance(files[0], os.PathLike) or os.path.isfile(files[0]):
return files
except Exception:
pass
- try: # glob pattern
+ try:
+ # glob pattern
+ assert isinstance(files[0], str)
files = natural_sorted(glob.glob(files[0]))
files[0] # noqa: validation
return files
@@ -597,7 +749,7 @@
raise ValueError('no files found') from exc
-def main(argv=None):
+def main(argv: list[str] | None = None) -> int:
"""Command line usage main function."""
if argv is None:
argv = sys.argv
@@ -607,35 +759,68 @@
parser = optparse.OptionParser(
usage='usage: %prog [options] files',
description='Convert volume data files to Chimera MAP files.',
- version=f'%prog {__version__}', prog='cmapfile')
+ version=f'%prog {__version__}',
+ prog='cmapfile',
+ )
opt = parser.add_option
opt('-q', '--quiet', dest='verbose', action='store_false', default=True)
- opt('--filetype', dest='filetype', default=None,
- help='type of input file(s), e.g. BIN, LSM, OIF, TIF')
- opt('--dtype', dest='dtype', default=None,
- help='type of data in BIN files. e.g. uint16')
- opt('--shape', dest='shape', default=None,
- help='shape of data in BIN files in F order, e.g. 256,256,32')
- opt('--offset', dest='offset', type='int', default=0,
- help='number of bytes to skip at beginning of BIN files')
- opt('--step', dest='step', default=None,
- help='stepsize of data in files in F order, e.g. 1.0,1.0,8.0')
- opt('--cmap', dest='cmap', default=None,
- help='name of output CMAP file')
- opt('--astype', dest='astype', default=None,
- help='type of data in CMAP file. e.g. float32')
- opt('--subsample', dest='subsample', type='int', default=16,
- help='write subsampled datasets to CMAP file')
+ opt(
+ '--filetype',
+ dest='filetype',
+ default=None,
+ help='type of input file(s), e.g. BIN, LSM, OIF, TIF',
+ )
+ opt(
+ '--dtype',
+ dest='dtype',
+ default=None,
+ help='type of data in BIN files. e.g. uint16',
+ )
+ opt(
+ '--shape',
+ dest='shape',
+ default=None,
+ help='shape of data in BIN files in F order, e.g. 256,256,32',
+ )
+ opt(
+ '--offset',
+ dest='offset',
+ type='int',
+ default=0,
+ help='number of bytes to skip at beginning of BIN files',
+ )
+ opt(
+ '--step',
+ dest='step',
+ default=None,
+ help='stepsize of data in files in F order, e.g. 1.0,1.0,8.0',
+ )
+ opt('--cmap', dest='cmap', default=None, help='name of output CMAP file')
+ opt(
+ '--astype',
+ dest='astype',
+ default=None,
+ help='type of data in CMAP file. e.g. float32',
+ )
+ opt(
+ '--subsample',
+ dest='subsample',
+ type='int',
+ default=16,
+ help='write subsampled datasets to CMAP file',
+ )
- options, files = parser.parse_args()
- if not files:
+ options, filesarg = parser.parse_args()
+ if not filesarg:
parser.error('no input files specified')
try:
- files = parse_files(files)
+ files = parse_files(filesarg)
+ if len(files) == 0:
+ raise ValueError
except ValueError:
parser.error('input file not found')
- shape = parse_numbers(options.shape, int)
+ shape = tuple(parse_numbers(options.shape, int))
if shape and len(shape) != 3:
parser.error('invalid shape: expected 3 integers')
shape = tuple(reversed(shape)) # C order
@@ -656,7 +841,8 @@
cmapfile=options.cmap,
astype=options.astype,
subsample=options.subsample,
- verbose=options.verbose)
+ verbose=options.verbose,
+ )
elif filetype in ('OIB', 'OIF'):
if len(files) > 1:
warnings.warn('too many input files')
@@ -666,7 +852,8 @@
cmapfile=options.cmap,
astype=options.astype,
subsample=options.subsample,
- verbose=options.verbose)
+ verbose=options.verbose,
+ )
elif filetype in ('TIF', 'TIFF'):
tif2cmap(
files,
@@ -674,13 +861,16 @@
cmapfile=options.cmap,
astype=options.astype,
subsample=options.subsample,
- verbose=options.verbose)
+ verbose=options.verbose,
+ )
elif filetype == 'CMAP':
if not step:
parser.error('no step size specified for CMAP file')
if options.verbose:
- print(f"Changing step size in '{os.path.basename(files[0])}'",
- flush=True)
+ print(
+ f"Changing step size in '{os.path.basename(files[0])}'",
+ flush=True,
+ )
with CmapFile(files[0], mode='r+') as cmap:
cmap.setstep(step)
elif options.dtype and options.shape:
@@ -693,7 +883,8 @@
cmapfile=options.cmap,
astype=options.astype,
subsample=options.subsample,
- verbose=options.verbose)
+ verbose=options.verbose,
+ )
else:
if not options.shape:
parser.error('no data shape specified')
@@ -702,6 +893,7 @@
parser.error(f'do not know how to convert {filetype} to CMAP')
if options.verbose:
print('Done.', flush=True)
+ return 0
if __name__ == '__main__':
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/cmapfile-2020.1.1/setup.py
new/cmapfile-2022.9.29/setup.py
--- old/cmapfile-2020.1.1/setup.py 2020-01-18 10:05:10.000000000 +0100
+++ new/cmapfile-2022.9.29/setup.py 2022-09-30 06:20:32.000000000 +0200
@@ -7,21 +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('cmapfile/cmapfile.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__',
- code, re.MULTILINE | re.DOTALL).groups()[0]
+readme = search(
+ r'(?:\r\n|\r|\n){2}"""(.*)"""(?:\r\n|\r|\n){2}[__version__|from]',
+ code,
+ re.MULTILINE | re.DOTALL,
+)
-readme = '\n'.join([description, '=' * len(description)] +
- readme.splitlines()[1:])
+readme = '\n'.join(
+ [description, '=' * len(description)] + readme.splitlines()[1:]
+)
-license = re.search(r'(# Copyright.*?(?:\r\n|\r|\n))(?:\r\n|\r|\n)+""', code,
- re.MULTILINE | re.DOTALL).groups()[0]
+license = search(
+ r'(# Copyright.*?(?:\r\n|\r|\n))(?:\r\n|\r|\n)+""',
+ code,
+ re.MULTILINE | re.DOTALL,
+)
license = license.replace('# ', '').replace('#', '')
@@ -35,20 +51,26 @@
setup(
name='cmapfile',
version=version,
+ license='BSD',
description=description,
long_description=readme,
author='Christoph Gohlke',
- author_email='[email protected]',
- url='https://www.lfd.uci.edu/~gohlke/',
- license='BSD',
+ author_email='[email protected]',
+ url='https://www.cgohlke.com',
+ project_urls={
+ 'Bug Tracker': 'https://github.com/cgohlke/cmapfile/issues',
+ 'Source Code': 'https://github.com/cgohlke/cmapfile',
+ # 'Documentation': 'https://',
+ },
packages=['cmapfile'],
- python_requires='>=3.6',
+ python_requires='>=3.8',
install_requires=[
- 'numpy>=1.14.5',
- 'scipy>=1.2',
- 'h5py>=2.9',
- 'tifffile>=2019.1.1',
- 'oiffile>=2020.1.1'],
+ 'numpy>=1.19.2',
+ 'scipy>=1.5',
+ 'h5py>=3.1',
+ 'tifffile>=2021.11.2',
+ 'oiffile>=2021.6.6',
+ ],
entry_points={'console_scripts': ['cmapfile = cmapfile:main']},
platforms=['any'],
classifiers=[
@@ -58,8 +80,9 @@
'Intended Audience :: Developers',
'Operating System :: OS Independent',
'Programming Language :: Python :: 3 :: Only',
- 'Programming Language :: Python :: 3.6',
- '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',
],
)