Repository: hyperspy/rosettasciio (language: python)

Issue:
Create a first version of the Readme file

Doc and import improvement
As discussed in https://github.com/hyperspy/rosettasciio/issues/38. This changes the import and docstring/documentation for the `bruker` and `blockfile` format as examples for discussion.

### Progress of the PR
- [x] Change implemented (can be split into several points),
- [x] update docstring (if appropriate),
- [x] update user guide (if appropriate),
- [ ] add an changelog entry in the `upcoming_changes` folder (see [`upcoming_changes/README.rst`](https://github.com/hyperspy/rosettasciio/blob/main/upcoming_changes/README.rst)),
- [ ] Check formatting changelog entry in the `readthedocs` doc build of this PR (link in github checks)
- [ ] add tests,
- [x] ready for review.

### Minimal example of the bug fix or the new feature
```python
from rsciio.msa import api
api.file_reader("your_msa_file.msa")
# Your new feature...
```
Note that this example can be useful to update the user guide.


Consolidate docstring and documentation
The problem is also, that it is not consistent how detailed the `file_reader` and `file_writer` docstrings are. For some formats more is documented in the user guide, for others more in the docstrings.

_Originally posted by @jlaehne in https://github.com/hyperspy/rosettasciio/pull/35#discussion_r968012172_


Discussion / hints:
# [Codecov](https://codecov.io/gh/hyperspy/rosettasciio/pull/2?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=hyperspy) Report
> Merging [#2](https://codecov.io/gh/hyperspy/rosettasciio/pull/2?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=hyperspy) (8a757f7) into [main](https://codecov.io/gh/hyperspy/rosettasciio/commit/a3539d5c72e6838053ac275eec677ee2d3c53688?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=hyperspy) (a3539d5) will **not change** coverage.
> The diff coverage is `n/a`.

```diff
@@           Coverage Diff           @@
##             main       #2   +/-   ##
=======================================
  Coverage   85.77%   85.77%           
=======================================
  Files          72       72           
  Lines       14689    14689           
=======================================
  Hits        12599    12599           
  Misses       2090     2090           
```



------

[Continue to review full report at Codecov](https://codecov.io/gh/hyperspy/rosettasciio/pull/2?src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=hyperspy).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=hyperspy)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/hyperspy/rosettasciio/pull/2?src=pr&el=footer&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=hyperspy). Last update [a3539d5...8a757f7](https://codecov.io/gh/hyperspy/rosettasciio/pull/2?src=pr&el=lastupdated&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=hyperspy). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=hyperspy).

Thanks for correcting it directly.



Fix patch:
diff --git a/docs/supported_formats/blockfile.rst b/docs/supported_formats/blockfile.rst
index 649b13bfa..7a78afb15 100644
--- a/docs/supported_formats/blockfile.rst
+++ b/docs/supported_formats/blockfile.rst
@@ -26,5 +26,9 @@ loading the file using a different mode. However, note that lazy loading
 does not support in-place writing (i.e lazy loading and the "r+" mode
 are incompatible).
 
+
+API functions
+^^^^^^^^^^^^^
+
 .. automodule:: rsciio.blockfile
    :members:
diff --git a/docs/supported_formats/bruker.rst b/docs/supported_formats/bruker.rst
index 1602d5e28..b0fcf3d12 100644
--- a/docs/supported_formats/bruker.rst
+++ b/docs/supported_formats/bruker.rst
@@ -22,6 +22,9 @@ The format contains an extensive list of details and parameters of EDS analyses
 which in `HyperSpy <https://hyperspy.org>`_ are mapped to the ``metadata`` and
 ``original_metadata`` dictionaries.
 
+
+API functions
+^^^^^^^^^^^^^
+
 .. automodule:: rsciio.bruker
    :members:
-
diff --git a/docs/supported_formats/digitalsurf.rst b/docs/supported_formats/digitalsurf.rst
index df0e61e4f..3223e64a1 100644
--- a/docs/supported_formats/digitalsurf.rst
+++ b/docs/supported_formats/digitalsurf.rst
@@ -13,3 +13,9 @@ metadata, as well as the loading of files containing multiple objects packed tog
 
 The plugin was developed based on the MountainsMap software documentation, which
 contains a description of the binary format.
+
+API functions
+^^^^^^^^^^^^^
+
+.. automodule:: rsciio.sur
+   :members:
diff --git a/docs/supported_formats/semper.rst b/docs/supported_formats/semper.rst
index bc8fdfac7..605b7f3de 100644
--- a/docs/supported_formats/semper.rst
+++ b/docs/supported_formats/semper.rst
@@ -11,3 +11,10 @@ binary format with an extensive header for up to 3 dimensional data.
 RosettaSciIO can read and write ``.unf``-files and will try to convert the data into a
 fitting BaseSignal subclass, based on the information stored in the label.
 Currently version 7 of the format should be fully supported.
+
+
+API functions
+^^^^^^^^^^^^^
+
+.. automodule:: rsciio.semper_unf
+   :members:
diff --git a/docs/supported_formats/tiff.rst b/docs/supported_formats/tiff.rst
index 575dadbb6..1bf2c7702 100644
--- a/docs/supported_formats/tiff.rst
+++ b/docs/supported_formats/tiff.rst
@@ -1,6 +1,6 @@
 .. _tiff-format:
 
-Tagges image file format (TIFF)
+Tagged image file format (TIFF)
 -------------------------------
 
 RosettaSciIO can read and write 2D and 3D ``.tiff`` files using using
@@ -23,54 +23,11 @@ RosettaSciIO can also import the scale and the units from ``.tiff`` files saved
 FEI, Zeiss SEM, Olympus SIS, Jeol SightX and Hamamatsu HPD-TA (streak camera)
 software.
 
-Extra loading arguments
-^^^^^^^^^^^^^^^^^^^^^^^
+API functions
+^^^^^^^^^^^^^
 
-- ``force_read_resolution`` (bool, optional): Force read image resolution using
-  the x_resolution, y_resolution and resolution_unit tags of the TIFF. Beware:
-  most software don't (properly) use these tags when saving TIFF files. Default
-  is ``False``.
-- ``hamamatsu_streak_axis_type`` (str, optional): decide the type of the
-  time axis for hamamatsu streak files:
-
-  * if ``uniform``, the best-fit linear axis is used, inducing a (small)
-    linearisation error. Initialise a UniformDataAxis.
-  * if ``data``, the raw time axis parsed from the metadata is used. Initialise
-    a DataAxis.
-  * if ``functional``, the best-fit 3rd-order polynomial axis is used, avoiding
-    linearisation error. Initialise a FunctionalDataAxis.
-
-By default, ``uniform`` is used but a warning of the linearisation error is issued.
-Explicitly passing ``hamamatsu_streak_axis_type='uniform'`` suppresses the warning.
-In all cases, the original axis values are stored in the ``original_metadata`` of the
-signal object.
-
-.. code-block:: python
-
-    >>> # Force read image resolution using the x_resolution, y_resolution and
-    >>> # the resolution_unit of the TIFF tags.
-    >>> s = hs.load('file.tif', force_read_resolution=True)
-    >>> # Load a non-uniform axis from a hamamatsu streak file:
-    >>> s = hs.load('file.tif', hamamatsu_streak_axis_type='data')
-
-
-Extra saving arguments
-^^^^^^^^^^^^^^^^^^^^^^
-
-- ``extratags`` (tuple, optional): save custom tags through the
-   ``tifffile`` library. Must conform to a specific convention
-   (see ``tifffile`` documentation and example below).
-
-.. code-block:: python
-
-    >>> # Saving the string 'Random metadata' in a custom tag (ID 65000)
-    >>> extratag = [(65000, 's', 1, "Random metadata", False)]
-    >>> s.save('file.tif', extratags=extratag)
-
-    >>> # Saving the string 'Random metadata' from a custom tag (ID 65000)
-    >>> s2 = hs.load('file.tif')
-    >>> s2.original_metadata['Number_65000']
-    b'Random metadata'
+.. automodule:: rsciio.tiff
+   :members:
 
 .. warning::
 
diff --git a/docs/supported_formats/tvips.rst b/docs/supported_formats/tvips.rst
index b476689a9..f05532189 100644
--- a/docs/supported_formats/tvips.rst
+++ b/docs/supported_formats/tvips.rst
@@ -27,45 +27,8 @@ will not work if no such file is found.
    parameters like the shape and scales of the scan dimensions should be
    manually recorded.
 
-Extra loading arguments
-^^^^^^^^^^^^^^^^^^^^^^^
-- ``scan_shape``: a tuple of integers to indicate the shape of the navigation
-  axes. For example, `(3, 4)` will have 3 scan points in the y direction and 4
-  in the x direction. If this argument is not provided, the data will be loaded
-  as a 1D stack of images. `auto` is also an option which aims to reconstruct
-  the scan based on the `rotidx` indices in frame headers. Since this only
-  works for square scan grids and is prone to failure, this option is not 
-  recommended.
-- ``scan_start_frame``: index of the first frame of the dataset to consider,
-  mainly relevant for 4D-STEM datasets. If `scan_shape="auto"` this parameter
-  is ignored.
-- ``winding_scan_axis``: if the acquisition software collected data without
-  beam flyback but with a winding "snake" scan, then every second scan row
-  or column needs to be reversed to make sense of the data. This can be
-  indicated with values `"x"` or `"y"`, depending on whether winding happened
-  along the primary or secondary axis. By default, flyback scan without winding
-  is assumed with `x` the fast scan and `y` the slow scan direction.
-- ``hysteresis``: if winding scan was active it is likely there is an overshoot
-  of a few pixels (2-5) every second scan row. This parameter shifts every
-  second row by the indicated number of scan points to align even and odd scan
-  rows. Default is 0, no hysteresis.
-- ``rechunking``: only relevant when using lazy loading. If set to `False`
-  each ``.tvips`` file is loaded as a single chunk. For a better experience, with the
-  default setting of `auto` rechunking is performed such that the navigation axes
-  are optimally chunked and the signal axes are not chunked. If set to anything else, the
-  value will be passed to the `chunks` argument in `dask.array.rechunk`.
-  
-Extra saving arguments
-^^^^^^^^^^^^^^^^^^^^^^
+API functions
+^^^^^^^^^^^^^
 
-- ``max_file_size``: approximate maximum size of individual files in bytes. 
-  In this way a dataset can be split into multiple files. A file needs to be
-  at least the size of the main header in the first file plus one frame and its
-  frame header. By default there is no maximum and the entire dataset is saved
-  to one file.
-- ``version``: ``.tvips`` file format version, defaults to version 2. Only version
-  1 and 2 are currently supported.
-- ``frame_header_extra_bytes``: the number of bytes to pad the frame headers
-  with. By default this is 0.
-- ``mode``: `1` for imaging, `2` for diffraction. By default the mode is
-  guessed from the signal type and signal unites.
+.. automodule:: rsciio.tvips
+   :members:
diff --git a/docs/supported_formats/usid.rst b/docs/supported_formats/usid.rst
index 21eae4dbe..eef73b608 100644
--- a/docs/supported_formats/usid.rst
+++ b/docs/supported_formats/usid.rst
@@ -21,42 +21,50 @@ If you have any questions regarding this module, please consider
 `contacting <https://pycroscopy.github.io/pyUSID/contact.html>`_
 the developers of pyUSID.
 
+.. Note::
+
+    h5USID files can contain multiple USID datasets within the same file.
+    RosettaSciIO supports reading in one or more USID datasets.
+
+.. Note::
+
+    When writing files with this plugin, the model and other secondary data
+    artifacts linked to the signal are not written to the file but these can be
+    implemented at a later stage.
+
 Requirements
 ^^^^^^^^^^^^
 
-1. Reading and writing h5USID files require the
-   `installation of pyUSID <https://pycroscopy.github.io/pyUSID/install.html>`_.
-2. Files must use the ``.h5`` file extension in order to use this IO plugin.
-   Using the ``.hdf5`` extension will default to the :ref:`HyperSpy plugin <hspy-format>`.
-
-Reading
-^^^^^^^
+Reading and writing h5USID files requires the
+`installation of pyUSID <https://pycroscopy.github.io/pyUSID/install.html>`_.
 
-h5USID files can contain multiple USID datasets within the same file.
-RosettaSciIO supports reading in one or more USID datasets.
+In `HyperSpy <https://hyperspy.org>`_, files must use the ``.h5`` file extension
+in order to use this IO
+plugin or the ``reader=usid_hdf5`` parameter of the ``load`` function needs to be
+set explicitly. Otherwise, using the ``.hdf5`` extension will default to the
+:ref:`HyperSpy plugin <hspy-format>`.
 
-Extra loading arguments
-+++++++++++++++++++++++
+API functions
+^^^^^^^^^^^^^
 
-- ``dataset_path``: str. Absolute path of USID Main HDF5 dataset.
-  (default is ``None`` - all USID Main Datasets will be read)
-- ``ignore_non_linear_dims``: bool, default is True. If True, parameters that
-  were varied non-linearly in the desired dataset will result in Exceptions.
-  Else, all such non-linearly varied parameters will be treated as
-  linearly varied parameters and a Signal object will be generated.
+.. automodule:: rsciio.usid_hdf5
+   :members:
 
+Usage examples
+^^^^^^^^^^^^^^
 
-Reading the sole dataset within a h5USID file:
+Reading the sole dataset within a h5USID file using `HyperSpy
+<https://hyperspy.org>`_:
 
 .. code-block:: python
 
     >>> hs.load("sample.h5")
     <Signal2D, title: HAADF, dimensions: (|128, 128)>
 
-If multiple datasets are present within the h5USID file and you try the same command again,
-**all** available datasets will be loaded.
+If multiple datasets are present within the h5USID file, **all** available
+datasets will be loaded.
 
-.. note::
+.. Note::
 
     Given that HDF5 files can accommodate very large datasets, setting ``lazy=True``
     is strongly recommended if the contents of the HDF5 file are not known apriori.
@@ -71,8 +79,9 @@ If multiple datasets are present within the h5USID file and you try the same com
     [<Signal2D, title: HAADF, dimensions: (|128, 128)>,
     <Signal1D, title: EELS, dimensions: (|64, 64, 1024)>]
 
-We can load a specific dataset using the ``dataset_path`` keyword argument. Setting it to the
-absolute path of the desired dataset will cause the single dataset to be loaded.
+We can load a specific dataset using the ``dataset_path`` keyword argument.
+Setting it to the absolute path of the desired dataset will cause the single
+dataset to be loaded.
 
 .. code-block:: python
 
@@ -80,12 +89,12 @@ absolute path of the desired dataset will cause the single dataset to be loaded.
     >>> hs.load("sample.h5", dataset_path='/Measurement_004/Channel_003/Main_Data')
     <Signal2D, title: HAADF, dimensions: (|128, 128)>
 
-h5USID files support the storage of HDF5 dataset with
-`compound data types <https://pycroscopy.github.io/USID/usid_model.html#compound-datasets>`_.
-As an (*oversimplified*) example, one could store a color image using a compound data type that allows
-each color channel to be accessed by name rather than an index.
-Naturally, reading in such a compound dataset into HyperSpy will result in a separate
-signal for each named component in the dataset:
+h5USID files support the storage of HDF5 dataset with `compound data types
+<https://pycroscopy.github.io/USID/usid_model.html#compound-datasets>`_. As an
+(*oversimplified*) example, one could store a color image using a compound data
+type that allows each color channel to be accessed by name rather than an index.
+Naturally, reading in such a compound dataset into HyperSpy will result in a
+separate signal for each named component in the dataset:
 
 .. code-block:: python
 
@@ -113,16 +122,3 @@ In order to prevent accidental misinterpretation of information downstream, the
     ValueError: Cannot load provided dataset. Parameter: Bias was varied non-uniformly.
     Supply keyword argument "ignore_non_uniform_dims=True" to ignore this error
 
-Writing
-^^^^^^^
-
-Signals can be written to new h5USID files using the standard :py:meth:`~.signal.BaseSignal.save` function.
-Setting the ``overwrite`` keyword argument to ``True`` will append to the specified
-HDF5 file. All other keyword arguments will be passed to :py:func:`pyUSID.io.hdf_utils.model.write_main_dataset`.
-
-.. code-block:: python
-
-    >>> sig.save("USID.h5")
-
-Note that the model and other secondary data artifacts linked to the signal are not
-written to the file but these can be implemented at a later stage.
diff --git a/docs/supported_formats/zspy.rst b/docs/supported_formats/zspy.rst
index b9686e742..129902076 100644
--- a/docs/supported_formats/zspy.rst
+++ b/docs/supported_formats/zspy.rst
@@ -47,35 +47,15 @@ To load this file again
     >>> store = zarr.LMDBStore(filename)
     >>> s = hs.load(store) # load from LMDB
 
-Extra saving arguments
-^^^^^^^^^^^^^^^^^^^^^^
-
-- ``compressor``: `Numcodecs codec <https://numcodecs.readthedocs.io/en/stable>`_,
-  a compressor can be passed to the save function to compress the data efficiently. The default
-  is to call a Blosc compressor object.
-
-    .. code-block:: python
-
-        >>> from numcodecs import Blosc
-        >>> compressor=Blosc(cname='zstd', clevel=1, shuffle=Blosc.SHUFFLE) # Used by default
-        >>> s.save('test.zspy', compressor = compressor) # will save with Blosc compression
-
-    .. note::
-
-        Lazy operations are often i-o bound, reading and writing the data creates a bottle neck in processes
-        due to the slow read write speed of many hard disks. In these cases, compressing your data is often
-        beneficial to the speed of some operations. Compression speeds up the process as there is less to
-        read/write with the trade off of slightly more computational work on the CPU.
-
-
-- ``chunks``: tuple of integer or None. Define the chunking used for saving
-  the dataset. If None, calculates chunks for the signal, with preferably at
-  least one chunk per signal space.
-- ``close_file``: only relevant for some zarr store (:py:class:`zarr.storage.ZipStore`, :py:class:`zarr.storage.DBMStore`)
-  requiring store to flush data to disk. If ``False``, doesn't close the file
-  after writing. The file should not be closed if the data need to be accessed
-  lazily after saving.
-  Default is ``True``.
-- ``write_dataset``: if ``False``, doesn't write the dataset when writing the file.
-  This can be useful to overwrite signal attributes only (for example ``axes_manager``)
-  without having to write the whole dataset, which can take time. Default is ``True``.
+API functions
+^^^^^^^^^^^^^
+
+.. automodule:: rsciio.zspy
+   :members:
+
+.. note::
+
+    Lazy operations are often i-o bound. Reading and writing the data creates a bottle neck in processes
+    due to the slow read write speed of many hard disks. In these cases, compressing your data is often
+    beneficial to the speed of some operations. Compression speeds up the process as there is less to
+    read/write with the trade off of slightly more computational work on the CPU.
diff --git a/rsciio/__init__.py b/rsciio/__init__.py
index 57ecda027..6ecacf188 100644
--- a/rsciio/__init__.py
+++ b/rsciio/__init__.py
@@ -31,7 +31,16 @@
         with open(_specsf, "r") as stream:
             _specs = yaml.safe_load(stream)
             # for testing purposes
-            if _specs["name"] in ["Blockfile", "BrukerComposite"]:
+            if _specs["name"] in [
+                "Blockfile",
+                "BrukerComposite",
+                "DigitalSurfSurface",
+                "Semper",
+                "TIFF",
+                "TVIPS",
+                "USID",
+                "ZSPY",
+            ]:
                 _specs["api"] = "rsciio.%s" % os.path.split(sub)[1]
             else:
                 _specs["api"] = "rsciio.%s.api" % os.path.split(sub)[1]
diff --git a/rsciio/blockfile/_api.py b/rsciio/blockfile/_api.py
index 00e80b4dd..20ce849e6 100644
--- a/rsciio/blockfile/_api.py
+++ b/rsciio/blockfile/_api.py
@@ -381,8 +381,6 @@ def file_writer(
         Whether to show the progressbar or not.
     endianess : str
         ``'<'`` (default) or ``'>'`` determining how the bits are written to the file
-
-    %s
     """
     smetadata = DTBox(signal["metadata"], box_dots=True)
     if intensity_scaling is None:
@@ -488,8 +486,4 @@ def file_writer(
     file_memmap.flush()
 
 
-file_writer.__doc__ %= (
-    FILENAME_DOC.replace("read", "write to"),
-    SIGNAL_DOC,
-    RETURNS_DOC,
-)
+file_writer.__doc__ %= (FILENAME_DOC.replace("read", "write to"), SIGNAL_DOC)
diff --git a/rsciio/bruker/__init__.py b/rsciio/bruker/__init__.py
index ef42b57d4..d4de92f67 100644
--- a/rsciio/bruker/__init__.py
+++ b/rsciio/bruker/__init__.py
@@ -1,4 +1,4 @@
-from rsciio.bruker._api import file_reader
+from ._api import file_reader
 
 
 __all__ = [
diff --git a/rsciio/bruker/_api.py b/rsciio/bruker/_api.py
index 5e941f7d2..172a6f3e2 100644
--- a/rsciio/bruker/_api.py
+++ b/rsciio/bruker/_api.py
@@ -43,7 +43,7 @@
 import dask.array as da
 import numpy as np
 
-from rsciio.docstrings import FILENAME_DOC, RETURNS_DOC
+from rsciio.docstrings import FILENAME_DOC, LAZY_DOC, RETURNS_DOC
 
 _logger = logging.getLogger(__name__)
 
@@ -1289,13 +1289,14 @@ def py_parse_hypermap(virtual_file, shape, dtype, downsample=1):
     return vfa
 
 
-def file_reader(filename, *args, **kwds):
+def file_reader(filename, lazy=False, *args, **kwds):
     """
     Read a Bruker ``.bcf`` or ``.spx`` file.
 
     Parameters
     ----------
     %s
+    %s
     select_type : ``'spectrum_image'``, ``'image'`` or None
         If specified, only the corresponding type of data, either spectrum
         image or image, is returned.
@@ -1352,22 +1353,22 @@ def file_reader(filename, *args, **kwds):
     """
     ext = splitext(filename)[1][1:]
     if ext == "bcf":
-        return bcf_reader(filename, *args, **kwds)
+        return bcf_reader(filename, lazy, *args, **kwds)
     elif ext == "spx":
-        return spx_reader(filename, *args, **kwds)
+        return spx_reader(filename, lazy, *args, **kwds)
 
 
-file_reader.__doc__ %= (FILENAME_DOC, RETURNS_DOC)
+file_reader.__doc__ %= (FILENAME_DOC, LAZY_DOC, RETURNS_DOC)
 
 
 def bcf_reader(
     filename,
+    lazy=False,
     select_type=None,
     index=None,  # noqa
     downsample=1,
     cutoff_at_kV=None,
     instrument=None,
-    lazy=False,
 ):
     """
     Reads a bruker ``.bcf`` file and loads the data into the appropriate class,
diff --git a/rsciio/docstrings.py b/rsciio/docstrings.py
index 670a054c3..065dad31f 100644
--- a/rsciio/docstrings.py
+++ b/rsciio/docstrings.py
@@ -22,22 +22,31 @@
 
 SIGNAL_DOC = """signal : dict
         Dictionary containing the signal object.
+        Should contain the following fields:
+
+        - 'data' – multidimensional numpy array
+        - 'axes' – list of dictionaries describing the axes
+          containing the fields 'name', 'units', 'index_in_array', and
+          either 'size', 'offset', and 'scale' or a numpy array 'axis'
+          containing the full axes vector
+        - 'metadata' – dictionary containing the metadata tree
     """
 
-LAZY_DOC = """lazy : bool
-        Whether to open the file lazily or not. Default is False.
+LAZY_DOC = """lazy : bool, Default=False
+        Whether to open the file lazily or not.
     """
 
 RETURNS_DOC = """Returns
     -------
 
-    List of dictionaries containing the following fields:
+    list of dicts
+        List of dictionaries containing the following fields:
 
-    - 'data' – multidimensional numpy array
-    - 'axes' – list of dictionaries describing the axes
-      containing the fields 'name', 'units', 'index_in_array', and
-      either 'size', 'offset', and 'scale' or a numpy array 'axis'
-      containing the full axes vector
-    - 'metadata' – dictionary containing the parsed metadata
-    - 'original_metadata' – dictionary containing the full metadata tree from the input file
+        - 'data' – multidimensional numpy array
+        - 'axes' – list of dictionaries describing the axes
+          containing the fields 'name', 'units', 'index_in_array', and
+          either 'size', 'offset', and 'scale' or a numpy array 'axis'
+          containing the full axes vector
+        - 'metadata' – dictionary containing the parsed metadata
+        - 'original_metadata' – dictionary containing the full metadata tree from the input file
     """
diff --git a/rsciio/semper_unf/__init__.py b/rsciio/semper_unf/__init__.py
index e69de29bb..61acf603f 100644
--- a/rsciio/semper_unf/__init__.py
+++ b/rsciio/semper_unf/__init__.py
@@ -0,0 +1,11 @@
+from ._api import file_reader, file_writer
+
+
+__all__ = [
+    "file_reader",
+    "file_writer",
+]
+
+
+def __dir__():
+    return sorted(__all__)
diff --git a/rsciio/semper_unf/api.py b/rsciio/semper_unf/_api.py
similarity index 97%
rename from rsciio/semper_unf/api.py
rename to rsciio/semper_unf/_api.py
index df530da28..36e636dcd 100644
--- a/rsciio/semper_unf/api.py
+++ b/rsciio/semper_unf/_api.py
@@ -81,6 +81,12 @@
 
 import numpy as np
 
+from rsciio.docstrings import (
+    FILENAME_DOC,
+    LAZY_DOC,
+    RETURNS_DOC,
+    SIGNAL_DOC,
+)
 from rsciio.utils.tools import sarray2dict, DTBox
 
 
@@ -545,7 +551,7 @@ def from_signal(cls, signal):
 
     def to_dictionary(self, lazy=False):
         """Export a :class:`~.SemperFormat` object to a
-        :class:`~hyperspy.signals.Signal` object.
+        signal dictionary.
 
         Parameters
         ----------
@@ -553,7 +559,7 @@ def to_dictionary(self, lazy=False):
 
         Returns
         -------
-        signal: :class:`~hyperspy.signals.Signal`
+        signal: dict
             The exported signal.
 
         """
@@ -663,13 +669,39 @@ def _read_data(fobj, fname, position, data_format, shape):
     return data
 
 
-def file_reader(filename, **kwds):
-    lazy = kwds.get("lazy", False)
+def file_reader(filename, lazy=False):
+    """
+    Read a Semper ``.unf`` file.
+
+    Parameters
+    ----------
+    %s
+    %s
+
+    %s
+    """
     semper = SemperFormat.load_from_unf(filename, lazy=lazy)
     semper.log_info()
     return [semper.to_dictionary(lazy=lazy)]
 
 
+file_reader.__doc__ %= (FILENAME_DOC, LAZY_DOC, RETURNS_DOC)
+
+
 def file_writer(filename, signal, **kwds):
+    """
+    Write signal to a Semper ``.unf`` file.
+
+    Parameters
+    ----------
+    %s
+    %s
+    skip_header : bool, default=False
+        Determines if the header, title and label should be skipped (useful
+        for some other programs).
+    """
     semper = SemperFormat.from_signal(signal)
     semper.save_to_unf(filename)
+
+
+file_writer.__doc__ %= (FILENAME_DOC.replace("read", "write to"), SIGNAL_DOC)
diff --git a/rsciio/sur/__init__.py b/rsciio/sur/__init__.py
index e69de29bb..d4de92f67 100644
--- a/rsciio/sur/__init__.py
+++ b/rsciio/sur/__init__.py
@@ -0,0 +1,10 @@
+from ._api import file_reader
+
+
+__all__ = [
+    "file_reader",
+]
+
+
+def __dir__():
+    return sorted(__all__)
diff --git a/rsciio/sur/api.py b/rsciio/sur/_api.py
similarity index 99%
rename from rsciio/sur/api.py
rename to rsciio/sur/_api.py
index a6204feb9..f477d6298 100644
--- a/rsciio/sur/api.py
+++ b/rsciio/sur/_api.py
@@ -49,6 +49,8 @@
 
 # DictionaryTreeBrowser class handles the fancy metadata dictionnaries
 # from hyperspy.misc.utils import DictionaryTreeBrowser
+
+from rsciio.docstrings import FILENAME_DOC, LAZY_DOC, RETURNS_DOC
 from rsciio.exceptions import MountainsMapFileError
 
 _logger = logging.getLogger(__name__)
@@ -1245,17 +1247,14 @@ def _pack_data(self, file, val, encoding="latin-1"):
 
 def file_reader(filename, **kwds):
     """Read a mountainsmap .sur file and return a dictionnary containing the
-    information necessary for creating the data object
+    information necessary for creating the data object.
 
     Parameters
     ----------
-    filename: name of the .sur file to be read
+    %s
+    %s
 
-    Returns
-    -------
-    signal_dict: dictionnary in the appropriate format. The dictionnary can
-    contain several keys including 'data', 'axes', 'metadata', 'original_metadata',
-    'post_process', 'mapping', 'attributes'.
+    %s
     """
 
     ds = DigitalSurfHandler(filename)
@@ -1267,3 +1266,6 @@ def file_reader(filename, **kwds):
     return [
         surdict,
     ]
+
+
+file_reader.__doc__ %= (FILENAME_DOC, LAZY_DOC, RETURNS_DOC)
diff --git a/rsciio/sur/specifications.yaml b/rsciio/sur/specifications.yaml
index 0514dcb91..3a4565c23 100644
--- a/rsciio/sur/specifications.yaml
+++ b/rsciio/sur/specifications.yaml
@@ -1,5 +1,5 @@
 name: DigitalSurfSurface
-name_aliases: [Digital Surf Surface]
+name_aliases: [DigitalSurf, Digital Surf Surface]
 description: Read data from the proprietary .sur file format from Digital Surf.
 full_support: False
 file_extensions: [sur, SUR, pro, PRO]
diff --git a/rsciio/tiff/__init__.py b/rsciio/tiff/__init__.py
index e69de29bb..61acf603f 100644
--- a/rsciio/tiff/__init__.py
+++ b/rsciio/tiff/__init__.py
@@ -0,0 +1,11 @@
+from ._api import file_reader, file_writer
+
+
+__all__ = [
+    "file_reader",
+    "file_writer",
+]
+
+
+def __dir__():
+    return sorted(__all__)
diff --git a/rsciio/tiff/api.py b/rsciio/tiff/_api.py
similarity index 93%
rename from rsciio/tiff/api.py
rename to rsciio/tiff/_api.py
index 8faf48df7..f74985d8c 100644
--- a/rsciio/tiff/api.py
+++ b/rsciio/tiff/_api.py
@@ -29,6 +29,12 @@
 from tifffile import imwrite, TiffFile, TIFF
 from tifffile import __version__ as tiffversion
 
+from rsciio.docstrings import (
+    FILENAME_DOC,
+    LAZY_DOC,
+    RETURNS_DOC,
+    SIGNAL_DOC,
+)
 from rsciio.utils.tools import DTBox, _UREG
 from rsciio.utils.date_time_tools import get_date_time_from_metadata
 
@@ -58,16 +64,33 @@
 
 
 def file_writer(filename, signal, export_scale=True, extratags=[], **kwds):
-    """Writes data to tif using Christoph Gohlke's tifffile library
+    """Writes data to tif using Christoph Gohlke's tifffile library.
 
     Parameters
     ----------
-    filename: str
-    signal: a BaseSignal instance
-    export_scale: bool
-        default: True
+    %s
+    %s
+    export_scale: bool, default=True
         Export the scale and the units (compatible with DM and ImageJ) to
         appropriate tags.
+    extratags: tuple, list of tuples, optional
+        Save custom tags through the ``tifffile`` library. Must conform to a
+        specific convention (see `tifffile documentation
+        <https://github.com/cgohlke/tifffile/#examples>`_ and example below).
+    **kwds: optional
+        Additional arguments to be passed to the ``imwrite`` function of the `tifffile library
+        <https://github.com/cgohlke/tifffile/#examples>`_.
+
+    Examples
+    --------
+    >>> # Saving the string 'Random metadata' in a custom tag (ID 65000)
+    >>> extratag = [(65000, 's', 1, "Random metadata", False)]
+    >>> s.save('file.tif', extratags=extratag)
+
+    >>> # Saving the string 'Random metadata' from a custom tag (ID 65000)
+    >>> s2 = hs.load('file.tif')
+    >>> s2.original_metadata['Number_65000']
+    b'Random metadata'
     """
 
     data = signal["data"]
@@ -103,7 +126,10 @@ def file_writer(filename, signal, export_scale=True, extratags=[], **kwds):
     imwrite(filename, data, software="hyperspy", photometric=photometric, **kwds)
 
 
-def file_reader(filename, force_read_resolution=False, lazy=False, **kwds):
+file_writer.__doc__ %= (FILENAME_DOC.replace("read", "write to"), SIGNAL_DOC)
+
+
+def file_reader(filename, lazy=False, force_read_resolution=False, **kwds):
     """
     Read data from tif files using Christoph Gohlke's tifffile library.
     The units and the scale of images saved with ImageJ or Digital
@@ -112,16 +138,40 @@ def file_reader(filename, force_read_resolution=False, lazy=False, **kwds):
 
     Parameters
     ----------
-    filename: str
-        Name of the file to read
-    force_read_resolution: bool
-        Force reading the x_resolution, y_resolution and the resolution_unit
-        of the tiff tags.
-        See https://www.awaresystems.be/imaging/tiff/tifftags/resolutionunit.html
-        Default is False.
-    lazy : bool
-        Load the data lazily. Default is False
-    **kwds, optional
+    %s
+    %s
+    force_read_resolution: bool, Default=False
+        Force read image resolution using the ``x_resolution``, ``y_resolution``
+        and ``resolution_unit`` tiff tags. Beware: most software don't (properly)
+        use these tags when saving ``.tiff`` files.
+        See `<https://www.awaresystems.be/imaging/tiff/tifftags/resolutionunit.html>`_.
+    hamamatsu_streak_axis_type: str, optional
+        Decide the type of the time axis for hamamatsu streak files:
+
+        - ``uniform``: the best-fit linear axis is used, inducing a (small)
+          linearisation error. Initialise a UniformDataAxis.
+        - ``data``: the raw time axis parsed from the metadata is used.
+          Initialise a DataAxis.
+        - ``functional``: the best-fit 3rd-order polynomial axis is used,
+          avoiding linearisation error. Initialise a FunctionalDataAxis.
+
+        By default, ``uniform`` is used but a warning of the linearisation error
+        is issued. Explicitly passing ``hamamatsu_streak_axis_type='uniform'``
+        suppresses the warning. In all cases, the original axis values are stored
+        in the ``original_metadata`` of the signal object.
+    **kwds: optional
+        Additional arguments to be passed to the ``TiffFile`` class of the `tifffile library
+        <https://github.com/cgohlke/tifffile/#examples>`_.
+
+    %s
+
+    Examples
+    --------
+    >>> # Force read image resolution using the x_resolution, y_resolution and
+    >>> # the resolution_unit of the TIFF tags.
+    >>> s = hs.load('file.tif', force_read_resolution=True)
+    >>> # Load a non-uniform axis from a hamamatsu streak file:
+    >>> s = hs.load('file.tif', hamamatsu_streak_axis_type='data')
     """
     tmp = kwds.pop("hamamatsu_streak_axis_type", None)
 
@@ -136,6 +186,9 @@ def file_reader(filename, force_read_resolution=False, lazy=False, **kwds):
     return dict_list
 
 
+file_reader.__doc__ %= (FILENAME_DOC, LAZY_DOC, RETURNS_DOC)
+
+
 def _order_axes_by_name(names: list, scales: dict, offsets: dict, units: dict):
     """order axes by names in lists"""
     scales_new = [1.0] * len(names)
diff --git a/rsciio/tvips/__init__.py b/rsciio/tvips/__init__.py
index e69de29bb..61acf603f 100644
--- a/rsciio/tvips/__init__.py
+++ b/rsciio/tvips/__init__.py
@@ -0,0 +1,11 @@
+from ._api import file_reader, file_writer
+
+
+__all__ = [
+    "file_reader",
+    "file_writer",
+]
+
+
+def __dir__():
+    return sorted(__all__)
diff --git a/rsciio/tvips/api.py b/rsciio/tvips/_api.py
similarity index 87%
rename from rsciio/tvips/api.py
rename to rsciio/tvips/_api.py
index 78b734338..d2b927c5d 100644
--- a/rsciio/tvips/api.py
+++ b/rsciio/tvips/_api.py
@@ -30,6 +30,12 @@
 import pint
 from numba import njit
 
+from rsciio.docstrings import (
+    FILENAME_DOC,
+    LAZY_DOC,
+    RETURNS_DOC,
+    SIGNAL_DOC,
+)
 from rsciio.utils.tools import DTBox, sarray2dict
 from rsciio.utils.tools import dummy_context_manager
 from rsciio.utils.tools import _UREG
@@ -212,11 +218,11 @@ def _guess_scan_index_grid(rotidx, start, stop):
 
 def file_reader(
     filename,
+    lazy=True,
     scan_shape=None,
     scan_start_frame=0,
     winding_scan_axis=None,
     hysteresis=0,
-    lazy=True,
     rechunking="auto",
     **kwds,
 ):
@@ -225,28 +231,43 @@ def file_reader(
 
     Parameters
     ----------
-    scan_shape : str or tuple of int
+    %s
+    %s
+    scan_shape : str or tuple of int, optional
         By default the data is loaded as an image stack (1 navigation axis).
-        If it concerns a 4D-STEM dataset, the (..., scan_y, scan_x) dimension can
-        be provided. "auto" can also be selected, in which case the rotidx
-        information in the frame headers will be used to try to reconstruct
-        the scan. Additional navigation axes must be prepended.
-    scan_start_frame : int
-        First frame where the scan starts. If scan_shape = "auto" this is
-        ignored.
-    winding_scan_axis : str
-        "x" or "y" if the scan was performed with winding scan along an axis
-        as opposed to flyback scan. By default (None), flyback scan is assumed
-        with "x" the fast and "y" the slow scan directions.
-    hysteresis: int
-        Only applicable if winding_scan_axis is not None. This parameter allows
-        every second column or row to be shifted to correct for hysteresis that
-        occurs during a winding scan.
-    rechunking: bool, str, or Dict
-        If set to False each tvips file is a single chunk. For a better experience
-        working with the dataset, an automatic rechunking is recommended (default).
-        If set to anything else, e.g. a Dictionary, the value will be passed to the
-        chunks argument in dask.array.rechunk.
+        A tuple of integers can be provided to indicate the shape of the
+        navigation axes. For example, `(3, 4)` will have 3 scan points in the y
+        direction and 4 in the x direction.
+        If it concerns a 4D-STEM dataset, the (..., ``scan_y``, ``scan_x)``
+        dimension can be provided. ``"auto"`` can also be selected, in which case
+        the ``rotidx`` information in the frame headers will be used to try to
+        reconstruct the scan. Additional navigation axes must be prepended. Since
+        this only works for square scan grids and is prone to failure, this option
+        is not recommended.
+    scan_start_frame : int, optional
+        Index of the first frame of the dataset to consider. Mainly relevant for
+        4D-STEM datasets.If ``scan_shape = "auto"`` this is ignored.
+    winding_scan_axis : str, optional
+        If the acquisition software collected data without beam flyback but with
+        a winding "snake" scan, then every second scan row or column needs to be
+        reversed to make sense of the data. This can be indicated with values
+        ``"x"`` or ``"y"``, depending on whether winding happened along the
+        primary or secondary axis. By default, flyback scan without winding
+        is assumed with ``x`` the fast scan and ``y`` the slow scan direction.
+    hysteresis: int, optional
+        Only applicable if ``winding_scan_axis`` is not ``None``, as it is likely
+        there is an overshoot of a few pixels (2-5) every second scan row. This
+        parameter allows shifts every second row by the indicated number of scan
+        points to align even and odd scan rows. Default is 0, no hysteresis.
+    rechunking: bool, str, or Dict, Default="auto"
+        Only relevant when using lazy loading. If set to False each tvips file is
+        a single chunk. For a better experience, with the default setting of
+        ``"auto"`` rechunking is performed such that the navigation axes
+        are optimally chunked and the signal axes are not chunked.
+        If set to anything else, e.g. a dictionary, the value will be passed to
+        the chunks argument in dask.array.rechunk.
+
+    %s
     """
     # check whether we start at the first tvips file
     _is_valid_first_tvips_file(filename)
@@ -482,28 +503,35 @@ def file_reader(
     ]
 
 
+file_reader.__doc__ %= (
+    FILENAME_DOC,
+    LAZY_DOC.replace("False", "True"),
+    RETURNS_DOC,
+)
+
+
 def file_writer(filename, signal, **kwds):
     """
     Write signal to TVIPS file.
 
     Parameters
     ----------
-    file: str
-        Filename of the file to write to. If not supplied, a _000 suffix will
-        automatically be appended before the extension.
-    signal : instance of hyperspy Signal2D
-        The signal to save.
+    %s
+    %s
     max_file_size: int, optional
-        Maximum size of individual files in bytes. By default there is no
-        maximum and everything is stored in a single file. Otherwise, files
-        are split into sequential parts denoted by a suffix _xxx starting
-        from _000.
+        Approximate maximum size of individual files in bytes. In this way a
+        dataset can be split into multiple files. A file needs to be at least the
+        size of the main header in the first file plus one frame and its frame
+        header. Sequential files are denoted by a suffix _xxx starting from _000.
+        By default there is no maximum and the entire dataset is stored in a
+        single file.
     version: int, optional
-        TVIPS file format version (1 or 2), defaults to 2
+        TVIPS file format version (only version ``1`` or ``2`` supported),
+        defaults to version ``2``.
     frame_header_extra_bytes: int, optional
         Number of bytes to pad the frame headers with, defaults to 0
     mode: int, optional
-        Imaging mode. 1 is imaging, 2 is diffraction. By default the mode is
+        ``1`` for imaging, ``2`` for diffraction. By default, the mode is
         guessed from signal type and signal units.
     """
     # only signal2d is allowed
@@ -622,3 +650,6 @@ def file_writer(filename, signal, **kwds):
         file_index += 1
         frames_to_save -= frames_saved
         current_frame += frames_saved
+
+
+file_writer.__doc__ %= (FILENAME_DOC.replace("read", "write to"), SIGNAL_DOC)
diff --git a/rsciio/usid_hdf5/__init__.py b/rsciio/usid_hdf5/__init__.py
index e69de29bb..61acf603f 100644
--- a/rsciio/usid_hdf5/__init__.py
+++ b/rsciio/usid_hdf5/__init__.py
@@ -0,0 +1,11 @@
+from ._api import file_reader, file_writer
+
+
+__all__ = [
+    "file_reader",
+    "file_writer",
+]
+
+
+def __dir__():
+    return sorted(__all__)
diff --git a/rsciio/usid_hdf5/api.py b/rsciio/usid_hdf5/_api.py
similarity index 88%
rename from rsciio/usid_hdf5/api.py
rename to rsciio/usid_hdf5/_api.py
index 232e3d6de..dfaf6f46e 100644
--- a/rsciio/usid_hdf5/api.py
+++ b/rsciio/usid_hdf5/_api.py
@@ -1,3 +1,21 @@
+# -*- coding: utf-8 -*-
+# Copyright 2007-2022 The HyperSpy developers
+#
+# This file is part of RosettaSciIO.
+#
+# RosettaSciIO is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# RosettaSciIO is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with RosettaSciIO. If not, see <https://www.gnu.org/licenses/#GPL>.
+
 import os
 import logging
 from warnings import warn
@@ -8,6 +26,14 @@
 import pyUSID as usid
 import sidpy
 
+from rsciio.docstrings import (
+    FILENAME_DOC,
+    LAZY_DOC,
+    RETURNS_DOC,
+    SIGNAL_DOC,
+)
+
+
 _logger = logging.getLogger(__name__)
 
 
@@ -377,31 +403,29 @@ def _axes_list_to_dimensions(axes_list, data_shape, is_spec):
 
 
 def file_reader(
-    filename, dataset_path=None, ignore_non_uniform_dims=True, lazy=False, **kwds
+    filename, lazy=False, dataset_path=None, ignore_non_uniform_dims=True, **kwds
 ):
     """
     Reads a USID Main dataset present in an HDF5 file into a HyperSpy Signal
 
     Parameters
     ----------
-    filename : str
-        path to HDF5 file
-    dataset_path : str, Optional
+    %s
+    %s
+    dataset_path : str, optional
         Absolute path of USID Main HDF5 dataset.
-        Default - None - all Main Datasets will be read. Given that HDF5 files
-        can accommodate very large datasets, lazy reading is strongly
+        Default is ``None`` - all Main Datasets will be read. Given that HDF5
+        files can accommodate very large datasets, lazy reading is strongly
         recommended.
-        If a string like ``'/Measurement_000/Channel_000/My_Dataset'`` is
+        If a string like ``"/Measurement_000/Channel_000/My_Dataset"`` is
         provided, the specific dataset will be loaded.
-    ignore_non_uniform_dims : bool, Optional
-        If True, parameters that were varied non-uniformly in the desired
-        dataset will result in Exceptions.
+    ignore_non_uniform_dims : bool, optional
+        If ``True`` (default), parameters that were varied non-uniformly in the
+        desired dataset will result in Exceptions.
         Else, all such non-uniformly varied parameters will be treated as
         uniformly varied parameters and a Signal object will be generated.
 
-    Returns
-    -------
-    list of hyperspy.signals.Signal object
+    %s
     """
     if not isinstance(filename, str):
         raise TypeError("filename should be a string")
@@ -435,33 +459,40 @@ def file_reader(
     return signals
 
 
-def file_writer(filename, object2save, **kwds):
+file_reader.__doc__ %= (FILENAME_DOC, LAZY_DOC, RETURNS_DOC)
+
+
+def file_writer(filename, signal, **kwds):
     """
-    Writes a HyperSpy Signal object to a HDF5 file formatted according to USID
+    Writes a HyperSpy Signal object to a HDF5 file formatted according to USID.
 
     Parameters
     ----------
-    filename : str
-        Path to target HDF5 file
-    object2save : hyperspy.signals.Signal
-        A HyperSpy signal
+    %s
+    %s
+    overwrite: bool, optional
+        If set to ``True``, the writer will append the data to the specified
+        HDF5 file.
+    **kwds: optional
+        All other keyword arguments will be passed to
+        :py:func:`pyUSID.io.hdf_utils.model.write_main_dataset`.
     """
     append = False
     if os.path.exists(filename):
         append = True
 
-    hs_shape = object2save["data"].shape
+    hs_shape = signal["data"].shape
 
-    parm_dict = _flatten_dict(object2save["metadata"])
-    temp = object2save["original_metadata"]
+    parm_dict = _flatten_dict(signal["metadata"])
+    temp = signal["original_metadata"]
     parm_dict.update(_flatten_dict(temp, parent_key="Original"))
 
-    axes = object2save["axes"]
+    axes = signal["axes"]
     nav_axes = [ax for ax in axes if ax["navigate"]][::-1]
     sig_axes = [ax for ax in axes if not ax["navigate"]][::-1]
     nav_dim = len(nav_axes)
 
-    data = object2save["data"]
+    data = signal["data"]
     # data is assumed to have dimensions arranged from slowest to fastest
     # varying dimensions
     if nav_dim > 0 and len(sig_axes) > 0:
@@ -516,3 +547,6 @@ def file_writer(filename, object2save, **kwds):
                 slow_to_fast=True,
                 **kwds,
             )
+
+
+file_writer.__doc__ %= (FILENAME_DOC.replace("read", "write to"), SIGNAL_DOC)
diff --git a/rsciio/zspy/__init__.py b/rsciio/zspy/__init__.py
index e69de29bb..61acf603f 100644
--- a/rsciio/zspy/__init__.py
+++ b/rsciio/zspy/__init__.py
@@ -0,0 +1,11 @@
+from ._api import file_reader, file_writer
+
+
+__all__ = [
+    "file_reader",
+    "file_writer",
+]
+
+
+def __dir__():
+    return sorted(__all__)
diff --git a/rsciio/zspy/api.py b/rsciio/zspy/_api.py
similarity index 78%
rename from rsciio/zspy/api.py
rename to rsciio/zspy/_api.py
index b800aa635..43bfb458f 100644
--- a/rsciio/zspy/api.py
+++ b/rsciio/zspy/_api.py
@@ -23,6 +23,12 @@
 import numcodecs
 import zarr
 
+from rsciio.docstrings import (
+    FILENAME_DOC,
+    LAZY_DOC,
+    RETURNS_DOC,
+    SIGNAL_DOC,
+)
 from rsciio._hierarchical import HierarchicalWriter, HierarchicalReader, version
 
 
@@ -104,28 +110,39 @@ def _store_data(data, dset, group, key, chunks):
 
 
 def file_writer(filename, signal, close_file=True, **kwds):
-    """Writes data to hyperspy's zarr format.
+    """Writes data to HyperSpy's zarr format.
 
     Parameters
     ----------
-    filename : str
-        The name of the file used to save the signal.
-    signal : a BaseSignal instance
-        The signal to save.
-    chunks : tuple of integer or None, default: None
+    %s
+    %s
+    close_file : bool, default=True
+        Close the file after writing. Only relevant for some zarr storages
+        (:py:class:`zarr.storage.ZipStore`, :py:class:`zarr.storage.DBMStore`)
+        requiring store to flush data to disk. If ``False``, doesn't close the
+        file after writing. The file should not be closed if the data needs to be
+        accessed lazily after saving.
+    chunks : tuple of integer or None, default=None
         Define the chunking used for saving the dataset. If None, calculates
         chunks for the signal, with preferably at least one chunk per signal
         space.
-    compressor : numcodecs compression
+    compressor : numcodecs compression, optional
+        A compressor can be passed to the save function to compress the data
+        efficiently, see `Numcodecs codec <https://numcodecs.readthedocs.io/en/stable>`_.
         The default is to use a Blosc compressor.
-    close_file : bool, default: True
-        Close the file after writing.
-    write_dataset : bool, default: True
-        If True, write the data, otherwise, don't write it. Useful to
-        save attributes without having to write the whole dataset.
+    write_dataset : bool, default=True
+        If ``False``, doesn't write the dataset when writing the file. This can
+        be useful to overwrite signal attributes only (for example ``axes_manager``)
+        without having to write the whole dataset, which can take time.
     **kwds
-        The keyword argument are passed to the
+        The keyword arguments are passed to the
         :py:meth:`zarr.hierarchy.Group.require_dataset` function.
+
+    Examples
+    --------
+    >>> from numcodecs import Blosc
+    >>> compressor=Blosc(cname='zstd', clevel=1, shuffle=Blosc.SHUFFLE) # Used by default
+    >>> s.save('test.zspy', compressor = compressor) # will save with Blosc compression
     """
     if "compressor" not in kwds:
         kwds["compressor"] = numcodecs.Blosc(
@@ -182,16 +199,21 @@ def file_writer(filename, signal, close_file=True, **kwds):
             store.flush()
 
 
+file_writer.__doc__ %= (FILENAME_DOC.replace("read", "write to"), SIGNAL_DOC)
+
+
 def file_reader(filename, lazy=False, **kwds):
-    """Read data from zspy files saved with the hyperspy zspy format
+    """Read data from zspy files saved with the HyperSpy zarr format
     specification.
 
     Parameters
     ----------
-    filename: str
-    lazy: bool
-        Load image lazily using dask
-    **kwds, optional
+    %s
+    %s
+    **kwds: optional
+        Pass keyword arguments to the :py:meth:`zarr.open` function.
+
+    %s
     """
     mode = kwds.pop("mode", "r")
     try:
@@ -207,3 +229,6 @@ def file_reader(filename, lazy=False, **kwds):
     reader = ZspyReader(f)
 
     return reader.read(lazy=lazy)
+
+
+file_reader.__doc__ %= (FILENAME_DOC, LAZY_DOC, RETURNS_DOC)


Test patch:
diff --git a/rsciio/tests/test_import.py b/rsciio/tests/test_import.py
index 8dcac07fb..d4ee6714d 100644
--- a/rsciio/tests/test_import.py
+++ b/rsciio/tests/test_import.py
@@ -103,8 +103,38 @@ def test_dir_plugins():
 
     assert dir(bruker) == ["file_reader"]
 
-    # skimage is optional dependencies
+    # skimage is an optional dependency
     pytest.importorskip("skimage")
     from rsciio import blockfile
 
     assert dir(blockfile) == ["file_reader", "file_writer"]
+
+    from rsciio import semper_unf
+
+    assert dir(semper_unf) == ["file_reader", "file_writer"]
+
+    from rsciio import sur
+
+    assert dir(sur) == ["file_reader"]
+
+    # tifffile is an optional dependency
+    pytest.importorskip("tifffile")
+    from rsciio import tiff
+
+    assert dir(tiff) == ["file_reader", "file_writer"]
+
+    from rsciio import tvips
+
+    assert dir(tvips) == ["file_reader", "file_writer"]
+
+    # pyUSID is an optional dependency
+    pytest.importorskip("pyUSID")
+    from rsciio import usid_hdf5
+
+    assert dir(usid_hdf5) == ["file_reader", "file_writer"]
+
+    # zarr is an optional dependency
+    pytest.importorskip("zarr")
+    from rsciio import zspy
+
+    assert dir(zspy) == ["file_reader", "file_writer"]
diff --git a/rsciio/tests/test_tiff.py b/rsciio/tests/test_tiff.py
index fda6d2fdd..170f00f54 100644
--- a/rsciio/tests/test_tiff.py
+++ b/rsciio/tests/test_tiff.py
@@ -414,7 +414,7 @@ def test_saving_loading_stack_no_scale():
             "0": {
                 "operation": "load",
                 "hyperspy_version": hs.__version__,
-                "io_plugin": "rsciio.tiff.api",
+                "io_plugin": "rsciio.tiff",
             }
         },
     },
@@ -444,7 +444,7 @@ def test_saving_loading_stack_no_scale():
         "date": "2022-05-17",
         "time": "09:07:08",
         "authors": "user",
-        "FileIO": {"0": {"operation": "load", "io_plugin": "rsciio.tiff.api"}},
+        "FileIO": {"0": {"operation": "load", "io_plugin": "rsciio.tiff"}},
     },
     "Signal": {"signal_type": ""},
     "Acquisition_instrument": {
@@ -589,7 +589,7 @@ def test_read_Zeiss_SEM_scale_metadata_1k_image(self):
                     "0": {
                         "operation": "load",
                         "hyperspy_version": hs.__version__,
-                        "io_plugin": "rsciio.tiff.api",
+                        "io_plugin": "rsciio.tiff",
                     }
                 },
             },
@@ -643,7 +643,7 @@ def test_read_Zeiss_SEM_scale_metadata_512_image(self):
                     "0": {
                         "operation": "load",
                         "hyperspy_version": hs.__version__,
-                        "io_plugin": "rsciio.tiff.api",
+                        "io_plugin": "rsciio.tiff",
                     }
                 },
             },
@@ -685,7 +685,7 @@ def test_read_RGB_Zeiss_optical_scale_metadata():
     assert s.metadata.General.date == "2016-06-13"
     assert s.metadata.General.time == "15:59:52"
     assert s.metadata.General.FileIO.Number_0.hyperspy_version == hs.__version__
-    assert s.metadata.General.FileIO.Number_0.io_plugin == "rsciio.tiff.api"
+    assert s.metadata.General.FileIO.Number_0.io_plugin == "rsciio.tiff"
 
 
 def test_read_BW_Zeiss_optical_scale_metadata():
@@ -762,7 +762,7 @@ def test_read_TVIPS_metadata():
                 "0": {
                     "operation": "load",
                     "hyperspy_version": hs.__version__,
-                    "io_plugin": "rsciio.tiff.api",
+                    "io_plugin": "rsciio.tiff",
                 }
             },
         },
@@ -982,22 +982,22 @@ def test_is_hamamatsu_streak(self):
 
         omd["Artist"] = "TAPTAP"
 
-        assert not rsciio.tiff.api._is_streak_hamamatsu(omd)
+        assert not rsciio.tiff._api._is_streak_hamamatsu(omd)
 
         _ = omd.pop("Artist")
 
-        assert not rsciio.tiff.api._is_streak_hamamatsu(omd)
+        assert not rsciio.tiff._api._is_streak_hamamatsu(omd)
 
         omd.update({"Artist": "Copyright Hamamatsu GmbH, 2018"})
 
         omd["Software"] = "TAPTAPTAP"
 
-        assert not rsciio.tiff.api._is_streak_hamamatsu(omd)
+        assert not rsciio.tiff._api._is_streak_hamamatsu(omd)
 
         _ = omd.pop("Software")
 
-        assert not rsciio.tiff.api._is_streak_hamamatsu(omd)
+        assert not rsciio.tiff._api._is_streak_hamamatsu(omd)
 
         omd.update({"Software": "HPD-TA 9.5 pf4"})
 
-        assert rsciio.tiff.api._is_streak_hamamatsu(omd)
+        assert rsciio.tiff._api._is_streak_hamamatsu(omd)
diff --git a/rsciio/tests/test_tvips.py b/rsciio/tests/test_tvips.py
index 35fb17419..07e4c5f57 100644
--- a/rsciio/tests/test_tvips.py
+++ b/rsciio/tests/test_tvips.py
@@ -32,7 +32,7 @@
 
 from hyperspy.misc.utils import DictionaryTreeBrowser
 
-from rsciio.tvips.api import (
+from rsciio.tvips._api import (
     _guess_image_mode,
     _get_main_header_from_signal,
     _get_frame_record_dtype_from_signal,