from __future__ import annotations
import warnings
from collections.abc import Sequence
from logging import getLogger
from typing import TYPE_CHECKING, Any
from napari.layers import Layer
from napari.plugins import _npe2
from napari.types import LayerData, PathLike
from napari.utils.translations import trans
logger = getLogger(__name__)
if TYPE_CHECKING:
from npe2.manifest.contributions import WriterContribution
def read_data_with_plugins(
paths: Sequence[PathLike],
plugin: str | None = None,
stack: bool = False,
) -> tuple[list[LayerData] | None, str | None]:
"""Iterate reader hooks and return first non-None LayerData or None.
This function returns as soon as the path has been read successfully,
while raising any errors occurring during the reading process, and
providing useful error messages.
Parameters
----------
paths : str, or list of string
The of path (file, directory, url) to open
plugin : str, optional
Name of a plugin to use. If provided, will force ``path`` to be read
with the specified ``plugin``. If the requested plugin cannot read
``path``, an exception will be raised.
stack : bool
See `Viewer.open`
Returns
-------
LayerData : list of tuples, or None
LayerData that can be passed to :func:`Viewer._add_layer_from_data()
<napari.components.viewer_model.ViewerModel._add_layer_from_data>`.
``LayerData`` is a list tuples, where each tuple is one of
``(data,)``, ``(data, meta)``, or ``(data, meta, layer_type)`` .
If no reader plugins were found (or they all failed), returns ``None``
reader : str, optional
The name of the reader plugin that was used, or None if no reader was found.
"""
if plugin == 'builtins':
warnings.warn(
trans._(
'The "builtins" plugin name is deprecated and will not work in a future version. Please use "napari" instead.',
deferred=True,
),
)
plugin = 'napari'
assert isinstance(paths, list)
if not stack:
assert len(paths) == 1
res = _npe2.read(paths, plugin, stack=stack)
if res is not None:
ld_, reader = res
return [] if _is_null_layer_sentinel(ld_) else list(ld_), reader
return [], None
[docs]
def save_layers(
path: str,
layers: list[Layer],
*,
plugin: str | None = None,
_writer: WriterContribution | None = None,
) -> list[str]:
"""Write list of layers or individual layer to a path using writer plugins.
If ``plugin`` is provided, will attempt to use a writer declared by that
plugin, assuming it is compatible with the given layer(s) and ``path``.
Otherwise, will use first compatible writer.
Parameters
----------
path : str
A filepath, directory, or URL to write.
layers : List[layers.Layer]
Non-empty List of layers to be saved. Warns when the list
of layers is empty.
plugin : str, optional
Name of the plugin to use for saving. If None then the first compatible
writer will be used.
Returns
-------
list of str
File paths of any files that were written.
"""
writer_name = ''
if layers:
written, writer_name = _write_layers_with_plugins(
path, layers, plugin_name=plugin, _writer=_writer
)
else:
warnings.warn(trans._('No layers to write.'))
return []
# If written is empty, something went wrong.
# Generate a warning to tell the user what it was.
if not written:
if writer_name:
warnings.warn(
trans._(
"Plugin '{name}' tried to save layers but did not return any written paths.",
deferred=True,
name=writer_name,
)
)
elif plugin:
warnings.warn(
trans._(
"Given plugin '{name}' is not a valid writer for {path}.",
deferred=True,
name=plugin,
path=path,
)
)
else:
warnings.warn(
trans._(
'No data written! A plugin could not be found to write these {length} layers to {path}.',
deferred=True,
length=len(layers),
path=path,
)
)
return written
def _is_null_layer_sentinel(layer_data: Any) -> bool:
"""Checks if the layer data returned from a reader function indicates an
empty file. The sentinel value used for this is ``[(None,)]``.
Parameters
----------
layer_data : LayerData
The layer data returned from a reader function to check
Returns
-------
bool
True, if the layer_data indicates an empty file, False otherwise
"""
return (
isinstance(layer_data, list)
and len(layer_data) == 1
and isinstance(layer_data[0], tuple)
and len(layer_data[0]) == 1
and layer_data[0][0] is None
)
def _write_layers_with_plugins(
path: str,
layers: list[Layer],
*,
plugin_name: str | None = None,
_writer: WriterContribution | None = None,
) -> tuple[list[str], str]:
"""Write data from multiple layers data with a plugin.
Calls out to _npe2.write_layers which will get the first
writer compatible with the given layer combination and specified file
extension in ``path``, and attempt to write the file(s).
Parameters
----------
path : str
The path (file, directory, url) to write.
layers : List of napari.layers.Layer
List of napari layers to write.
plugin_name : str, optional
If provided, force the plugin manager to use this plugin's writer.
If none is available, or if it is
incapable of handling the layers, this function will fail.
Returns
-------
(written paths, writer name) as Tuple[List[str],str]
written paths: List[str]
Empty list when no plugin was found, otherwise a list of file paths,
if any, that were written.
writer name: str
Name of the plugin selected to write the data.
"""
written_paths, writer_name = _npe2.write_layers(
path, layers, plugin_name, _writer
)
if written_paths or writer_name:
return (written_paths, writer_name)
return ([], '')