Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Deprecate skimage.io plugin infrastructure #7353

Draft
wants to merge 5 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
12 changes: 12 additions & 0 deletions TODO.txt
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,18 @@ Version 0.25
and remove `test_gaussian.py::test_deprecated_gaussian_output`.
Make arguments after the deprecated `output` parameter, keyword only:
`gaussian(image, sigma, *, ...)`.
* Complete deprecation of plugin infrastructure by removing the following:
- skimage/io/_plugins
- skimage/io/manage_plugins.py
- skimage/io/__init__.py::{
_hide_repeated_plugin_deprecation_warnings,
_deprecate_plugin_function,
__getattr__
}
- skimage/io/_io.py::{imread_collection, imshow, imshow_collection, show}
- TODO ...
as well as related tests, imports, etc...


Version 0.26
------------
Expand Down
7 changes: 7 additions & 0 deletions skimage/_shared/testing.py
Original file line number Diff line number Diff line change
Expand Up @@ -303,6 +303,13 @@ def setup_test():
category=UserWarning,
)

warnings.filterwarnings(
action="ignore",
message=".*Use imageio or a similar package instead.*",
category=FutureWarning,
module="skimage",
)


def teardown_test():
"""Default package level teardown routine for skimage tests.
Expand Down
120 changes: 72 additions & 48 deletions skimage/io/__init__.py
Original file line number Diff line number Diff line change
@@ -1,65 +1,89 @@
"""Utilities to read and write images in various formats.

The following plug-ins are available:

"""

from .manage_plugins import *
from .sift import *
from .collection import *

from ._io import *
from ._image_stack import *


reset_plugins()
import warnings
import functools
from contextlib import contextmanager

WRAP_LEN = 73
from .._shared.utils import deprecate_func


def _separator(char, lengths):
return [char * separator_length for separator_length in lengths]
@contextmanager
def _hide_repeated_plugin_deprecation_warnings():
"""Ignore warnings related to plugin infrastructure deprecation."""
with warnings.catch_warnings():
warnings.filterwarnings(
action="ignore",
message=".*Use imageio or a similar package instead.*",
category=FutureWarning,
module="skimage",
)
yield


def _format_plugin_info_table(info_table, column_lengths):
"""Add separators and column titles to plugin info table."""
info_table.insert(0, _separator('=', column_lengths))
info_table.insert(1, ('Plugin', 'Description'))
info_table.insert(2, _separator('-', column_lengths))
info_table.append(_separator('=', column_lengths))
def _deprecate_plugin_function(func):
"""Mark a function of the plugin infrastructure as deprecated.


def _update_doc(doc):
"""Add a list of plugins to the module docstring, formatted as
a ReStructuredText table.
In addition to emitting the appropriate FutureWarning, this also supresses
identical warnings that might be caused when this function calls other
functions from the deprecated plugin infrastructure.
"""
from textwrap import wrap

info_table = [
(p, plugin_info(p).get('description', 'no description'))
for p in available_plugins
if not p == 'test'
]
@deprecate_func(
deprecated_version="0.23",
removed_version="0.25",
hint="Use imageio or a similar package instead.",
)
@functools.wraps(func)
def wrapper(*args, **kwargs):
with _hide_repeated_plugin_deprecation_warnings():
return func(*args, **kwargs)

if len(info_table) > 0:
name_length = max([len(n) for (n, _) in info_table])
else:
name_length = 0
return wrapper

description_length = WRAP_LEN - 1 - name_length
column_lengths = [name_length, description_length]
_format_plugin_info_table(info_table, column_lengths)

for name, plugin_description in info_table:
description_lines = wrap(plugin_description, description_length)
name_column = [name]
name_column.extend(['' for _ in range(len(description_lines) - 1)])
for name, description in zip(name_column, description_lines):
doc += f"{name.ljust(name_length)} {description}\n"
doc = doc.strip()
from .manage_plugins import *
from .sift import *
from .collection import *

return doc
from ._io import *
from ._image_stack import *


if __doc__ is not None:
__doc__ = _update_doc(__doc__)
__all__ = [
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Probably want to hide items from __all__ that are deprecated.

'use_plugin',
'call_plugin',
'plugin_info',
'plugin_order',
'reset_plugins',
'find_available_plugins',
'available_plugins',
'load_sift',
'load_surf',
'MultiImage',
'ImageCollection',
'concatenate_images',
'imread_collection_wrapper',
'imread',
'imsave',
'imshow',
'show',
'imread_collection',
'imshow_collection',
'image_stack',
'push',
'pop',
]


def __getattr__(name):
if name == "available_plugins":
warnings.warn(
"`available_plugins` is deprecated since version 0.23 and will "
"be removed in version 0.25. Use imageio or a similar package "
"instead.",
category=FutureWarning,
stacklevel=2,
)
return globals()["_available_plugins"]
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
140 changes: 111 additions & 29 deletions skimage/io/_io.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,17 @@
import pathlib
import warnings

import numpy as np
import imageio.v3 as iio

from .._shared.utils import warn
from .._shared.utils import warn, deprecate_parameter, DEPRECATED
from .._shared.version_requirements import require
from ..exposure import is_low_contrast
from ..color.colorconv import rgb2gray, rgba2rgb
from ..io.manage_plugins import call_plugin
from ..io.manage_plugins import call_plugin, plugin_order
from .util import file_or_url_context

from . import _deprecate_plugin_function

__all__ = [
'imread',
Expand All @@ -20,7 +23,17 @@
]


def imread(fname, as_gray=False, plugin=None, **plugin_args):
@deprecate_parameter(
deprecated_name="plugin",
start_version="0.23",
stop_version="0.25",
template="Parameter `{deprecated_name}` is deprecated since version "
"{deprecated_version} and will be removed in {changed_version} (or "
"later). To avoid this warning, please do not use the parameter "
"`{deprecated_name}`. Use imageio or other 3rd party libraries directly "
"for more advanced IO features.",
)
def imread(fname, as_gray=False, plugin=DEPRECATED, **plugin_args):
"""Load an image from file.

Parameters
Expand All @@ -30,16 +43,13 @@ def imread(fname, as_gray=False, plugin=None, **plugin_args):
as_gray : bool, optional
If True, convert color images to gray-scale (64-bit floats).
Images that are already in gray-scale format are not converted.
plugin : str, optional
Name of plugin to use. By default, the different plugins are
tried (starting with imageio) until a suitable
candidate is found. If not given and fname is a tiff file, the
tifffile plugin will be used.

Other Parameters
----------------
plugin_args : keywords
Passed to the given plugin.
**plugin_args : DEPRECATED
`plugin_args` is deprecated.

.. deprecated:: 0.23

Returns
-------
Expand All @@ -49,15 +59,42 @@ def imread(fname, as_gray=False, plugin=None, **plugin_args):
RGB-image MxNx3 and an RGBA-image MxNx4.

"""
if isinstance(fname, pathlib.Path):
fname = str(fname.resolve())
if plugin_args:
warnings.warn(
"Additional keyword arguments are deprecated since version "
"0.23 and will be removed in 0.25 (or later). To avoid this "
"warning, please do not use additional keyword arguments. "
"Use imageio or a similar package instead.",
category=FutureWarning,
stacklevel=2,
)

if plugin is None and hasattr(fname, 'lower'):
if fname.lower().endswith(('.tiff', '.tif')):
plugin = 'tifffile'
with warnings.catch_warnings():
warnings.filterwarnings(
action="ignore",
message=".*Use imageio or a similar package instead.*",
category=FutureWarning,
module="skimage.io",
)
use_plugin = (
plugin is not DEPRECATED
or plugin_args
or plugin_order()["imread"][0] != "imageio"
)
if use_plugin:
Comment on lines +79 to +84
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's been proven quite tricky to preserve imread and imsave while deprecating the underlying plugin infrastructure. Honestly, I feel like it's not worth the deprecation headache to keep simple wrappers around but for now I've attempted the following approach:

  • Deprecate usage of the parameters plugin and plugin_args. plugin is also a parameter of imageio which we are planning to wrap. So keeping them around while not silently changing the behavior of user code seems impossible without changing the namespace.
  • If plugin or plugin_args are given or "imageiois not the default plugin (e.g.use_plugin("tifffile")` was used before), use the old plugin infra path. Otherwise simply wrap imageio.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why not just pop the imageio plugin imread and imwrite into the main module? We do not need to maintain the underlying plugin infrastructure.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Because we want a deprecation period during which we support both APIs (our old one and imageio), right?

I'm probably overthinking this. Let me look into this again and try for a simpler approach.

plugin = None if plugin is DEPRECATED else plugin
if isinstance(fname, pathlib.Path):
fname = str(fname.resolve())

if plugin is None and hasattr(fname, 'lower'):
if fname.lower().endswith(('.tiff', '.tif')):
plugin = 'tifffile'

with file_or_url_context(fname) as fname:
img = call_plugin('imread', fname, plugin=plugin, **plugin_args)
with file_or_url_context(fname) as fname:
img = call_plugin('imread', fname, plugin=plugin, **plugin_args)
else:
with file_or_url_context(fname) as fname:
img = iio.imread(fname)

if not hasattr(img, 'ndim'):
return img
Expand All @@ -75,6 +112,7 @@ def imread(fname, as_gray=False, plugin=None, **plugin_args):
return img


@_deprecate_plugin_function
def imread_collection(load_pattern, conserve_memory=True, plugin=None, **plugin_args):
"""
Load a collection of images.
Expand Down Expand Up @@ -105,7 +143,17 @@ def imread_collection(load_pattern, conserve_memory=True, plugin=None, **plugin_
)


def imsave(fname, arr, plugin=None, check_contrast=True, **plugin_args):
@deprecate_parameter(
deprecated_name="plugin",
start_version="0.23",
stop_version="0.25",
template="Parameter `{deprecated_name}` is deprecated since version "
"{deprecated_version} and will be removed in {changed_version} (or "
"later). To avoid this warning, please do not use the parameter "
"`{deprecated_name}`. Use imageio or other 3rd party libraries directly "
"for more advanced IO features.",
)
def imsave(fname, arr, plugin=DEPRECATED, check_contrast=True, **plugin_args):
"""Save an image to file.

Parameters
Expand Down Expand Up @@ -135,24 +183,52 @@ def imsave(fname, arr, plugin=None, check_contrast=True, **plugin_args):
and largest file size (default 75). This is only available when using
the PIL and imageio plugins.
"""
if isinstance(fname, pathlib.Path):
fname = str(fname.resolve())
if plugin is None and hasattr(fname, 'lower'):
if fname.lower().endswith(('.tiff', '.tif')):
plugin = 'tifffile'
if plugin_args:
warnings.warn(
"Additional keyword arguments are deprecated since version "
"0.23 and will be removed in 0.25 (or later). To avoid this "
"warning, please do not use additional keyword arguments. "
"Use imageio or a similar package instead.",
category=FutureWarning,
stacklevel=2,
)

if arr.dtype == bool:
warn(
f'{fname} is a boolean image: setting True to 255 and False to 0. '
'To silence this warning, please convert the image using '
'img_as_ubyte.',
stacklevel=2,
stacklevel=3,
)
arr = arr.astype('uint8') * 255
if check_contrast and is_low_contrast(arr):
warn(f'{fname} is a low contrast image')
return call_plugin('imsave', fname, arr, plugin=plugin, **plugin_args)
warn(f'{fname} is a low contrast image', stacklevel=3)

with warnings.catch_warnings():
warnings.filterwarnings(
action="ignore",
message=".*Use imageio or a similar package instead.*",
category=FutureWarning,
module="skimage.io",
)
use_plugin = (
plugin is not DEPRECATED
or plugin_args
or plugin_order()["imsave"][0] != "imageio"
)
if use_plugin:
plugin = None if plugin is DEPRECATED else plugin
if isinstance(fname, pathlib.Path):
fname = str(fname.resolve())
if plugin is None and hasattr(fname, 'lower'):
if fname.lower().endswith(('.tiff', '.tif')):
plugin = 'tifffile'
return call_plugin('imsave', fname, arr, plugin=plugin, **plugin_args)

return iio.imwrite(fname, arr)


@_deprecate_plugin_function
def imshow(arr, plugin=None, **plugin_args):
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

imshow and similar don't have to be deprecated. I could try to wrap matplotlib in a similar manner to wrapping imageio in im{save,read}. However, before investing the work, I'll first wait if we decide that it's worth keeping them around...

"""Display an image.

Expand All @@ -170,11 +246,14 @@ def imshow(arr, plugin=None, **plugin_args):
Passed to the given plugin.

"""
if isinstance(arr, str):
arr = call_plugin('imread', arr, plugin=plugin)
return call_plugin('imshow', arr, plugin=plugin, **plugin_args)
with warnings.catch_warnings():
warnings.simplefilter("ignore")
if isinstance(arr, str):
arr = call_plugin('imread', arr, plugin=plugin)
return call_plugin('imshow', arr, plugin=plugin, **plugin_args)


@_deprecate_plugin_function
def imshow_collection(ic, plugin=None, **plugin_args):
"""Display a collection of images.

Expand All @@ -192,9 +271,12 @@ def imshow_collection(ic, plugin=None, **plugin_args):
Passed to the given plugin.

"""
from ..io.manage_plugins import call_plugin

return call_plugin('imshow_collection', ic, plugin=plugin, **plugin_args)


@_deprecate_plugin_function
@require("matplotlib", ">=3.3")
def show():
"""Display pending images.
Expand Down