From 74473e8440f37c04ad061a585ddf55fc7c9c0e2c Mon Sep 17 00:00:00 2001
From: Vincent Delbar <vincent.delbar@latelescop.fr>
Date: Fri, 3 Nov 2023 16:57:27 +0100
Subject: [PATCH 01/30] ENH: remove useless encoding declaration

---
 pyotb/apps.py      | 1 -
 pyotb/core.py      | 1 -
 pyotb/functions.py | 1 -
 pyotb/helpers.py   | 1 -
 4 files changed, 4 deletions(-)

diff --git a/pyotb/apps.py b/pyotb/apps.py
index cd0c696..e5523a2 100644
--- a/pyotb/apps.py
+++ b/pyotb/apps.py
@@ -1,4 +1,3 @@
-# -*- coding: utf-8 -*-
 """Search for OTB (set env if necessary), subclass core.App for each available application."""
 from __future__ import annotations
 
diff --git a/pyotb/core.py b/pyotb/core.py
index d05c7d5..6f8c24c 100644
--- a/pyotb/core.py
+++ b/pyotb/core.py
@@ -1,4 +1,3 @@
-# -*- coding: utf-8 -*-
 """This module is the core of pyotb."""
 from __future__ import annotations
 
diff --git a/pyotb/functions.py b/pyotb/functions.py
index 96431cd..31e366f 100644
--- a/pyotb/functions.py
+++ b/pyotb/functions.py
@@ -1,4 +1,3 @@
-# -*- coding: utf-8 -*-
 """This module provides a set of functions for pyotb."""
 from __future__ import annotations
 
diff --git a/pyotb/helpers.py b/pyotb/helpers.py
index 3fc93b3..b6c09b3 100644
--- a/pyotb/helpers.py
+++ b/pyotb/helpers.py
@@ -1,4 +1,3 @@
-# -*- coding: utf-8 -*-
 """This module ensure we properly initialize pyotb, or raise SystemExit in case of broken install."""
 import logging
 import os
-- 
GitLab


From 50a51fd20e9d05b14816c6d520520a2ea58f4983 Mon Sep 17 00:00:00 2001
From: Vincent Delbar <vincent.delbar@latelescop.fr>
Date: Fri, 3 Nov 2023 16:58:46 +0100
Subject: [PATCH 02/30] BUG: remove corner case for OTB < 7.4

---
 pyotb/functions.py | 12 +++++-------
 1 file changed, 5 insertions(+), 7 deletions(-)

diff --git a/pyotb/functions.py b/pyotb/functions.py
index 31e366f..5eff2d4 100644
--- a/pyotb/functions.py
+++ b/pyotb/functions.py
@@ -457,12 +457,10 @@ def define_processing_area(
             #  It should replace any 'outside' pixel by some NoData -> add `fillvalue` argument in the function
 
         # Applying this bounding box to all inputs
+        bounds = (ulx, uly, lrx, lry)
         logger.info(
             "Cropping all images to extent Upper Left (%s, %s), Lower Right (%s, %s)",
-            ulx,
-            uly,
-            lrx,
-            lry,
+            *bounds,
         )
         new_inputs = []
         for inp in inputs:
@@ -472,11 +470,11 @@ def define_processing_area(
                     "mode": "extent",
                     "mode.extent.unit": "phy",
                     "mode.extent.ulx": ulx,
-                    "mode.extent.uly": lry,  # bug in OTB <= 7.3 :
+                    "mode.extent.uly": uly,
                     "mode.extent.lrx": lrx,
-                    "mode.extent.lry": uly,  # ULY/LRY are inverted
+                    "mode.extent.lry": lry,
                 }
-                new_input = App("ExtractROI", params)
+                new_input = App("ExtractROI", params, quiet=True)
                 # TODO: OTB 7.4 fixes this bug, how to handle different versions of OTB?
                 new_inputs.append(new_input)
                 # Potentially update the reference inputs for later resampling
-- 
GitLab


From e670786a81614e0596ba9b722a31f68bd3606b70 Mon Sep 17 00:00:00 2001
From: Vincent Delbar <vincent.delbar@latelescop.fr>
Date: Fri, 3 Nov 2023 16:59:25 +0100
Subject: [PATCH 03/30] ENH: update functions.py docstrings and light refac

---
 pyotb/functions.py | 151 ++++++++++++++++++++-------------------------
 1 file changed, 66 insertions(+), 85 deletions(-)

diff --git a/pyotb/functions.py b/pyotb/functions.py
index 5eff2d4..ad2f24c 100644
--- a/pyotb/functions.py
+++ b/pyotb/functions.py
@@ -8,25 +8,28 @@ import sys
 import textwrap
 import uuid
 from collections import Counter
+from pathlib import Path
 
 from .core import App, Input, LogicalOperation, Operation, get_nbchannels
 from .helpers import logger
 
 
-def where(
-    cond: App | str, x: App | str | int | float, y: App | str | int | float
-) -> Operation:
+def where(cond: App | str, x: App | str | float, y: App | str | float) -> Operation:
     """Functionally similar to numpy.where. Where cond is True (!=0), returns x. Else returns y.
 
+    If cond is monoband whereas x or y are multiband, cond channels are expanded to match x & y ones.
+
     Args:
-        cond: condition, must be a raster (filepath, App, Operation...). If cond is monoband whereas x or y are
-              multiband, cond channels are expanded to match x & y ones.
+        cond: condition, must be a raster (filepath, App, Operation...).
         x: value if cond is True. Can be: float, int, App, filepath, Operation...
         y: value if cond is False. Can be: float, int, App, filepath, Operation...
 
     Returns:
         an output where pixels are x if cond is True, else y
 
+    Raises:
+        ValueError: if x and y have different number of bands
+
     """
     # Checking the number of bands of rasters. Several cases :
     # - if cond is monoband, x and y can be multibands. Then cond will adapt to match x and y nb of bands
@@ -61,19 +64,13 @@ def where(
             " of channels of condition to match the number of channels of X/Y",
             x_or_y_nb_channels,
         )
-
     # Get the number of bands of the result
-    if x_or_y_nb_channels:  # if X or Y is a raster
-        out_nb_channels = x_or_y_nb_channels
-    else:  # if only cond is a raster
-        out_nb_channels = cond_nb_channels
+    out_nb_channels = x_or_y_nb_channels or cond_nb_channels
 
     return Operation("?", cond, x, y, nb_bands=out_nb_channels)
 
 
-def clip(
-    image: App | str, v_min: App | str | int | float, v_max: App | str | int | float
-):
+def clip(image: App | str, v_min: App | str | float, v_max: App | str | float):
     """Clip values of image in a range of values.
 
     Args:
@@ -85,19 +82,18 @@ def clip(
         raster whose values are clipped in the range
 
     """
-    if isinstance(image, str):
+    if isinstance(image, (str, Path)):
         image = Input(image)
-    res = where(image <= v_min, v_min, where(image >= v_max, v_max, image))
-    return res
+    return where(image <= v_min, v_min, where(image >= v_max, v_max, image))
 
 
 def all(*inputs):  # pylint: disable=redefined-builtin
     """Check if value is different than 0 everywhere along the band axis.
 
-    For only one image, this function checks that all bands of the image are True (i.e. !=0) and outputs
-    a singleband boolean raster
+    For only one image, this function checks that all bands of the image are True (i.e. !=0)
+     and outputs a singleband boolean raster
     For several images, this function checks that all images are True (i.e. !=0) and outputs
-    a boolean raster, with as many bands as the inputs
+     a boolean raster, with as many bands as the inputs
 
     Args:
         inputs: inputs can be 1) a single image or 2) several images, either passed as separate arguments
@@ -132,18 +128,18 @@ def all(*inputs):  # pylint: disable=redefined-builtin
                 res = res & inp[:, :, band]
             else:
                 res = res & (inp[:, :, band] != 0)
+        return res
+
     # Checking that all images are True
+    if isinstance(inputs[0], LogicalOperation):
+        res = inputs[0]
     else:
-        if isinstance(inputs[0], LogicalOperation):
-            res = inputs[0]
+        res = inputs[0] != 0
+    for inp in inputs[1:]:
+        if isinstance(inp, LogicalOperation):
+            res = res & inp
         else:
-            res = inputs[0] != 0
-        for inp in inputs[1:]:
-            if isinstance(inp, LogicalOperation):
-                res = res & inp
-            else:
-                res = res & (inp != 0)
-
+            res = res & (inp != 0)
     return res
 
 
@@ -158,6 +154,7 @@ def any(*inputs):  # pylint: disable=redefined-builtin
     Args:
         inputs: inputs can be 1) a single image or 2) several images, either passed as separate arguments
                 or inside a list
+
     Returns:
         OR intersection
 
@@ -188,19 +185,18 @@ def any(*inputs):  # pylint: disable=redefined-builtin
                 res = res | inp[:, :, band]
             else:
                 res = res | (inp[:, :, band] != 0)
+        return res
 
     # Checking that at least one image is True
+    if isinstance(inputs[0], LogicalOperation):
+        res = inputs[0]
     else:
-        if isinstance(inputs[0], LogicalOperation):
-            res = inputs[0]
+        res = inputs[0] != 0
+    for inp in inputs[1:]:
+        if isinstance(inp, LogicalOperation):
+            res = res | inp
         else:
-            res = inputs[0] != 0
-        for inp in inputs[1:]:
-            if isinstance(inp, LogicalOperation):
-                res = res | inp
-            else:
-                res = res | (inp != 0)
-
+            res = res | (inp != 0)
     return res
 
 
@@ -224,17 +220,19 @@ def run_tf_function(func):
     Returns:
         wrapper: a function that returns a pyotb object
 
+    Raises:
+        SystemError: if OTBTF apps are missing
+
     """
     try:
         from .apps import (  # pylint: disable=import-outside-toplevel
             TensorflowModelServe,
         )
-    except ImportError:
-        logger.error(
+    except ImportError as err:
+        raise SystemError(
             "Could not run Tensorflow function: failed to import TensorflowModelServe."
             "Check that you have OTBTF configured (https://github.com/remicres/otbtf#how-to-install)"
-        )
-        raise
+        ) from err
 
     def get_tf_pycmd(output_dir, channels, scalar_inputs):
         """Create a string containing all python instructions necessary to create and save the Keras model.
@@ -301,13 +299,13 @@ def run_tf_function(func):
         raster_inputs = []
         for inp in inputs:
             try:
-                # this is for raster input
+                # This is for raster input
                 channel = get_nbchannels(inp)
                 channels.append(channel)
                 scalar_inputs.append(None)
                 raster_inputs.append(inp)
             except TypeError:
-                # this is for other inputs (float, int)
+                # This is for other inputs (float, int)
                 channels.append(None)
                 scalar_inputs.append(inp)
 
@@ -316,6 +314,7 @@ def run_tf_function(func):
         out_savedmodel = os.path.join(tmp_dir, f"tmp_otbtf_model_{uuid.uuid4()}")
         pycmd = get_tf_pycmd(out_savedmodel, channels, scalar_inputs)
         cmd_args = [sys.executable, "-c", pycmd]
+        # TODO: remove subprocess execution since this issues has been fixed with OTBTF 4.0
         try:
             subprocess.run(
                 cmd_args,
@@ -407,41 +406,25 @@ def define_processing_area(
     # TODO: there seems to have a bug, ImageMetaData is not updated when running an app,
     #  cf https://gitlab.orfeo-toolbox.org/orfeotoolbox/otb/-/issues/2234. Should we use ImageOrigin instead?
     if not all(
-        metadata["UpperLeftCorner"] == any_metadata["UpperLeftCorner"]
-        and metadata["LowerRightCorner"] == any_metadata["LowerRightCorner"]
-        for metadata in metadatas.values()
+        md["UpperLeftCorner"] == any_metadata["UpperLeftCorner"]
+        and md["LowerRightCorner"] == any_metadata["LowerRightCorner"]
+        for md in metadatas.values()
     ):
         # Retrieving the bounding box that will be common for all inputs
         if window_rule == "intersection":
             # The coordinates depend on the orientation of the axis of projection
             if any_metadata["GeoTransform"][1] >= 0:
-                ulx = max(
-                    metadata["UpperLeftCorner"][0] for metadata in metadatas.values()
-                )
-                lrx = min(
-                    metadata["LowerRightCorner"][0] for metadata in metadatas.values()
-                )
+                ulx = max(md["UpperLeftCorner"][0] for md in metadatas.values())
+                lrx = min(md["LowerRightCorner"][0] for md in metadatas.values())
             else:
-                ulx = min(
-                    metadata["UpperLeftCorner"][0] for metadata in metadatas.values()
-                )
-                lrx = max(
-                    metadata["LowerRightCorner"][0] for metadata in metadatas.values()
-                )
+                ulx = min(md["UpperLeftCorner"][0] for md in metadatas.values())
+                lrx = max(md["LowerRightCorner"][0] for md in metadatas.values())
             if any_metadata["GeoTransform"][-1] >= 0:
-                lry = min(
-                    metadata["LowerRightCorner"][1] for metadata in metadatas.values()
-                )
-                uly = max(
-                    metadata["UpperLeftCorner"][1] for metadata in metadatas.values()
-                )
+                lry = min(md["LowerRightCorner"][1] for md in metadatas.values())
+                uly = max(md["UpperLeftCorner"][1] for md in metadatas.values())
             else:
-                lry = max(
-                    metadata["LowerRightCorner"][1] for metadata in metadatas.values()
-                )
-                uly = min(
-                    metadata["UpperLeftCorner"][1] for metadata in metadatas.values()
-                )
+                lry = max(md["LowerRightCorner"][1] for md in metadatas.values())
+                uly = min(md["UpperLeftCorner"][1] for md in metadatas.values())
 
         elif window_rule == "same_as_input":
             ulx = metadatas[reference_window_input]["UpperLeftCorner"][0]
@@ -449,12 +432,12 @@ def define_processing_area(
             lry = metadatas[reference_window_input]["LowerRightCorner"][1]
             uly = metadatas[reference_window_input]["UpperLeftCorner"][1]
         elif window_rule == "specify":
-            pass
-            # TODO : it is when the user explicitly specifies the bounding box -> add some arguments in the function
+            # TODO : when the user explicitly specifies the bounding box -> add some arguments in the function
+            ...
         elif window_rule == "union":
-            pass
-            # TODO : it is when the user wants the final bounding box to be the union of all bounding box
+            # TODO : when the user wants the final bounding box to be the union of all bounding box
             #  It should replace any 'outside' pixel by some NoData -> add `fillvalue` argument in the function
+            ...
 
         # Applying this bounding box to all inputs
         bounds = (ulx, uly, lrx, lry)
@@ -478,16 +461,14 @@ def define_processing_area(
                 # TODO: OTB 7.4 fixes this bug, how to handle different versions of OTB?
                 new_inputs.append(new_input)
                 # Potentially update the reference inputs for later resampling
-                if str(inp) == str(
-                    reference_pixel_size_input
-                ):  # we use comparison of string because calling '=='
+                if str(inp) == str(reference_pixel_size_input):
+                    # We use comparison of string because calling '=='
                     # on pyotb objects implicitly calls BandMathX application, which is not desirable
                     reference_pixel_size_input = new_input
-            except RuntimeError as e:
-                logger.error(
-                    "Cannot define the processing area for input %s: %s", inp, e
-                )
-                raise
+            except RuntimeError as err:
+                raise ValueError(
+                    f"Cannot define the processing area for input {inp}"
+                ) from err
         inputs = new_inputs
         # Update metadatas
         metadatas = {input: input.app.GetImageMetaData("out") for input in inputs}
@@ -496,9 +477,9 @@ def define_processing_area(
     any_metadata = next(iter(metadatas.values()))
     # Handling different pixel sizes
     if not all(
-        metadata["GeoTransform"][1] == any_metadata["GeoTransform"][1]
-        and metadata["GeoTransform"][5] == any_metadata["GeoTransform"][5]
-        for metadata in metadatas.values()
+        md["GeoTransform"][1] == any_metadata["GeoTransform"][1]
+        and md["GeoTransform"][5] == any_metadata["GeoTransform"][5]
+        for md in metadatas.values()
     ):
         # Retrieving the pixel size that will be common for all inputs
         if pixel_size_rule == "minimal":
-- 
GitLab


From e277304176fc09824667ceec2620651bbde51f0d Mon Sep 17 00:00:00 2001
From: Vincent Delbar <vincent.delbar@latelescop.fr>
Date: Fri, 3 Nov 2023 16:59:54 +0100
Subject: [PATCH 04/30] ENH: rename pyOTB logger to just pyotb

---
 pyotb/helpers.py | 6 +++---
 1 file changed, 3 insertions(+), 3 deletions(-)

diff --git a/pyotb/helpers.py b/pyotb/helpers.py
index b6c09b3..e0feb63 100644
--- a/pyotb/helpers.py
+++ b/pyotb/helpers.py
@@ -15,13 +15,13 @@ DOCS_URL = "https://www.orfeo-toolbox.org/CookBook/Installation.html"
 # Logging
 # User can also get logger with `logging.getLogger("pyOTB")`
 # then use pyotb.set_logger_level() to adjust logger verbosity
-logger = logging.getLogger("pyOTB")
+logger = logging.getLogger("pyotb")
 logger_handler = logging.StreamHandler(sys.stdout)
 formatter = logging.Formatter(
-    fmt="%(asctime)s (%(levelname)-4s) [pyOTB] %(message)s", datefmt="%Y-%m-%d %H:%M:%S"
+    fmt="%(asctime)s (%(levelname)-4s) [pyotb] %(message)s", datefmt="%Y-%m-%d %H:%M:%S"
 )
 logger_handler.setFormatter(formatter)
-# Search for PYOTB_LOGGER_LEVEL, else use OTB_LOGGER_LEVEL as pyOTB level, or fallback to INFO
+# Search for PYOTB_LOGGER_LEVEL, else use OTB_LOGGER_LEVEL as pyotb level, or fallback to INFO
 LOG_LEVEL = (
     os.environ.get("PYOTB_LOGGER_LEVEL") or os.environ.get("OTB_LOGGER_LEVEL") or "INFO"
 )
-- 
GitLab


From a21aa4dfbcb7212184c3226652c36d5dc1e2abb7 Mon Sep 17 00:00:00 2001
From: Vincent Delbar <vincent.delbar@latelescop.fr>
Date: Fri, 3 Nov 2023 17:01:34 +0100
Subject: [PATCH 05/30] ENH: refactor duplicated function in Operation

---
 pyotb/core.py | 69 ++++++++++++++++++++++-----------------------------
 1 file changed, 30 insertions(+), 39 deletions(-)

diff --git a/pyotb/core.py b/pyotb/core.py
index 6f8c24c..d56e9e9 100644
--- a/pyotb/core.py
+++ b/pyotb/core.py
@@ -1266,6 +1266,32 @@ class Operation(App):
             appname, il=self.unique_inputs, exp=self.exp, quiet=True, name=name
         )
 
+    def get_nb_bands(self, inputs: list[OTBObject | str | int | float]) -> int:
+        """Guess the number of bands of the output image, from the inputs.
+
+        Args:
+            inputs: the Operation operands
+
+        Raises:
+            ValueError: if all inputs don't have the same number of bands
+
+        """
+        if any(
+            isinstance(inp, Slicer) and hasattr(inp, "one_band_sliced")
+            for inp in inputs
+        ):
+            return 1
+        # Check that all inputs have the same band count
+        nb_bands_list = [
+            get_nbchannels(inp)
+            for inp in inputs
+            if not isinstance(inp, (float, int))
+        ]
+        all_same = all(x == nb_bands_list[0] for x in nb_bands_list)
+        if len(nb_bands_list) > 1 and not all_same:
+            raise ValueError("All images do not have the same number of bands")
+        return nb_bands_list[0]
+
     def build_fake_expressions(
         self,
         operator: str,
@@ -1285,29 +1311,12 @@ class Operation(App):
         self.inputs.clear()
         self.nb_channels.clear()
         logger.debug("%s, %s", operator, inputs)
-        # This is when we use the ternary operator with `pyotb.where` function. The output nb of bands is already known
+        # When we use the ternary operator with `pyotb.where` function, the output nb of bands is already known
         if operator == "?" and nb_bands:
             pass
         # For any other operations, the output number of bands is the same as inputs
         else:
-            if any(
-                isinstance(inp, Slicer) and hasattr(inp, "one_band_sliced")
-                for inp in inputs
-            ):
-                nb_bands = 1
-            else:
-                nb_bands_list = [
-                    get_nbchannels(inp)
-                    for inp in inputs
-                    if not isinstance(inp, (float, int))
-                ]
-                # check that all inputs have the same nb of bands
-                if len(nb_bands_list) > 1 and not all(
-                    x == nb_bands_list[0] for x in nb_bands_list
-                ):
-                    raise ValueError("All images do not have the same number of bands")
-                nb_bands = nb_bands_list[0]
-
+            nb_bands = self.get_nb_bands(inputs)
         # Create a list of fake expressions, each item of the list corresponding to one band
         self.fake_exp_bands.clear()
         for i, band in enumerate(range(1, nb_bands + 1)):
@@ -1463,29 +1472,11 @@ class LogicalOperation(Operation):
         Args:
             operator: str (one of >, <, >=, <=, ==, !=, &, |)
             inputs: Can be OTBObject, filepath, int or float
-            nb_bands: to specify the output nb of bands. Optional. Used only internally by pyotb.where
+            nb_bands: optionnaly specify the output nb of bands - used only internally by pyotb.where
 
         """
-        # For any other operations, the output number of bands is the same as inputs
-        if any(
-            isinstance(inp, Slicer) and hasattr(inp, "one_band_sliced")
-            for inp in inputs
-        ):
-            nb_bands = 1
-        else:
-            nb_bands_list = [
-                get_nbchannels(inp)
-                for inp in inputs
-                if not isinstance(inp, (float, int))
-            ]
-            # check that all inputs have the same nb of bands
-            if len(nb_bands_list) > 1 and not all(
-                x == nb_bands_list[0] for x in nb_bands_list
-            ):
-                raise ValueError("All images do not have the same number of bands")
-            nb_bands = nb_bands_list[0]
         # Create a list of fake exp, each item of the list corresponding to one band
-        for i, band in enumerate(range(1, nb_bands + 1)):
+        for i, band in enumerate(range(1, self.get_nb_bands(inputs) + 1)):
             expressions = []
             for inp in inputs:
                 fake_exp, corresp_inputs, nb_channels = super().make_fake_exp(
-- 
GitLab


From 04d3c20d8441c637d6cbe5801a139b5f52670926 Mon Sep 17 00:00:00 2001
From: Vincent Delbar <vincent.delbar@latelescop.fr>
Date: Fri, 3 Nov 2023 17:02:55 +0100
Subject: [PATCH 06/30] ENH: add more exceptions to prevent errors during
 __set_param

---
 pyotb/core.py | 11 ++++++++++-
 1 file changed, 10 insertions(+), 1 deletion(-)

diff --git a/pyotb/core.py b/pyotb/core.py
index d56e9e9..2a385ca 100644
--- a/pyotb/core.py
+++ b/pyotb/core.py
@@ -1043,9 +1043,18 @@ class App(OTBObject):
                 else:  # here `input` should be an image filepath
                     # Append `input` to the list, do not overwrite any previously set element of the image list
                     self.app.AddParameterStringList(key, inp)
+                else:
+                    raise TypeError(
+                        f"{self.name}: wrong input parameter type ({type(inp)})"
+                        f" found in '{key}' list: {inp}"
+                    )
         # List of any other types (str, int...)
-        else:
+        elif self.is_key_list(key):
             self.app.SetParameterValue(key, obj)
+        else:
+            raise TypeError(
+                f"{self.name}: wrong input parameter type ({type(obj)}) for '{key}'"
+            )
 
     def __sync_parameters(self):
         """Save app parameters in _auto_parameters or data dict.
-- 
GitLab


From 9bd6c3e5f19d55f10a78ebb2d34146bcfbbad4f3 Mon Sep 17 00:00:00 2001
From: Vincent Delbar <vincent.delbar@latelescop.fr>
Date: Fri, 3 Nov 2023 17:11:05 +0100
Subject: [PATCH 07/30] DOC: update or enh comments and docstrings, add
 "Raises", fix type hints

---
 pyotb/apps.py         |  18 +--
 pyotb/core.py         | 301 ++++++++++++++++++++++--------------------
 pyotb/depreciation.py |  10 +-
 pyotb/functions.py    |  17 +--
 pyotb/helpers.py      |  22 ++-
 pyotb/install.py      |   9 +-
 6 files changed, 202 insertions(+), 175 deletions(-)

diff --git a/pyotb/apps.py b/pyotb/apps.py
index e5523a2..45e49c4 100644
--- a/pyotb/apps.py
+++ b/pyotb/apps.py
@@ -12,12 +12,12 @@ from .helpers import logger
 def get_available_applications() -> tuple[str]:
     """Find available OTB applications.
 
-    Args:
-        as_subprocess: indicate if function should list available applications using subprocess call
-
     Returns:
         tuple of available applications
 
+    Raises:
+        SystemExit: if no application is found
+
     """
     app_list = otb.Registry.GetAvailableApplications()
     if app_list:
@@ -29,7 +29,7 @@ def get_available_applications() -> tuple[str]:
 
 
 class OTBTFApp(App):
-    """Helper for OTBTF."""
+    """Helper for OTBTF to ensure the nb_sources variable is set."""
 
     @staticmethod
     def set_nb_sources(*args, n_sources: int = None):
@@ -59,10 +59,8 @@ class OTBTFApp(App):
 
         Args:
             name: name of the OTBTF app
-            *args: arguments (dict). NB: we don't need kwargs because it cannot contain source#.il
             n_sources: number of sources. Default is None (resolves the number of sources based on the
                        content of the dict passed in args, where some 'source' str is found)
-            **kwargs: kwargs
 
         """
         self.set_nb_sources(*args, n_sources=n_sources)
@@ -71,16 +69,12 @@ class OTBTFApp(App):
 
 AVAILABLE_APPLICATIONS = get_available_applications()
 
-# This is to enable aliases of Apps, i.e. using apps like `pyotb.AppName(...)` instead of `pyotb.App("AppName", ...)`
-_CODE_TEMPLATE = (
-    """
+# This is to enable aliases of Apps, i.e. `pyotb.AppName(...)` instead of `pyotb.App("AppName", ...)`
+_CODE_TEMPLATE = """
 class {name}(App):
-    """
-    """
     def __init__(self, *args, **kwargs):
         super().__init__('{name}', *args, **kwargs)
 """
-)
 
 for _app in AVAILABLE_APPLICATIONS:
     # Customize the behavior for some OTBTF applications. `OTB_TF_NSOURCES` is now handled by pyotb
diff --git a/pyotb/core.py b/pyotb/core.py
index 2a385ca..0f27547 100644
--- a/pyotb/core.py
+++ b/pyotb/core.py
@@ -21,12 +21,12 @@ class OTBObject(ABC):
     @property
     @abstractmethod
     def name(self) -> str:
-        """By default, should return the application name, but a custom name may be passed during init."""
+        """Application name by default, but a custom name may be passed during init."""
 
     @property
     @abstractmethod
     def app(self) -> otb.Application:
-        """Reference to the main (or last in pipeline) otb.Application instance linked to this object."""
+        """Reference to the otb.Application instance linked to this object."""
 
     @property
     @abstractmethod
@@ -41,11 +41,11 @@ class OTBObject(ABC):
     @property
     @abstractmethod
     def exports_dic(self) -> dict[str, dict]:
-        """Return an internal dict object containing np.array exports, to avoid duplicated ExportImage() calls."""
+        """Ref to an internal dict of np.array exports, to avoid duplicated ExportImage()."""
 
     @property
     def metadata(self) -> dict[str, (str, float, list[float])]:
-        """Return metadata.
+        """Return image metadata as dictionary.
 
         The returned dict results from the concatenation of the first output
         image metadata dictionary and the metadata dictionary.
@@ -61,10 +61,7 @@ class OTBObject(ABC):
             if getattr(otb_imd, "has")(key)
         }
 
-        # Metadata dictionary
-        # Replace items like {"metadata_1": "TIFFTAG_SOFTWARE=CSinG - 13
-        # SEPTEMBRE 2012"} with {"TIFFTAG_SOFTWARE": "CSinG - 13 SEPTEMBRE
-        # 2012"}
+        # Other metadata dictionary: key-value pairs parsing is required
         mdd = dict(self.app.GetMetadataDictionary(self.output_image_key))
         new_mdd = {}
         for key, val in mdd.items():
@@ -104,7 +101,9 @@ class OTBObject(ABC):
 
     @property
     def transform(self) -> tuple[int]:
-        """Get image affine transform, rasterio style (see https://www.perrygeo.com/python-affine-transforms.html).
+        """Get image affine transform, rasterio style.
+
+        See https://www.perrygeo.com/python-affine-transforms.html
 
         Returns:
             transform: (X spacing, X offset, X origin, Y offset, Y spacing, Y origin)
@@ -116,7 +115,7 @@ class OTBObject(ABC):
         return spacing_x, 0.0, origin_x, 0.0, spacing_y, origin_y
 
     def summarize(self, *args, **kwargs):
-        """Recursively summarize parameters and parents.
+        """Recursively summarize an app parameters and its parents.
 
         Args:
             *args: args for `pyotb.summarize()`
@@ -138,7 +137,7 @@ class OTBObject(ABC):
 
     def get_values_at_coords(
         self, row: int, col: int, bands: int = None
-    ) -> list[int | float] | int | float:
+    ) -> list[float] | float:
         """Get pixel value(s) at a given YX coordinates.
 
         Args:
@@ -149,6 +148,9 @@ class OTBObject(ABC):
         Returns:
             single numerical value or a list of values for each band
 
+        Raises:
+            TypeError: if bands is not a slice or list
+
         """
         channels = []
         app = App("PixelValue", self, coordx=col, coordy=row, frozen=True, quiet=True)
@@ -170,8 +172,19 @@ class OTBObject(ABC):
         data = literal_eval(app.app.GetParameterString("value"))
         return data[0] if len(channels) == 1 else data
 
-    def channels_list_from_slice(self, bands: int) -> list[int]:
-        """Get list of channels to read values at, from a slice."""
+    def channels_list_from_slice(self, bands: slice) -> list[int]:
+        """Get list of channels to read values at, from a slice.
+
+        Args:
+            bands: slice obtained when using app[:]
+
+        Returns:
+            list of channels to select
+
+        Raises:
+            ValueError: if the slice is malformed
+
+        """
         nb_channels = self.shape[2]
         start, stop, step = bands.start, bands.stop, bands.step
         start = nb_channels + start if isinstance(start, int) and start < 0 else start
@@ -226,7 +239,7 @@ class OTBObject(ABC):
                   required to True if preserve_dtype is False and the source app reference is lost
 
         Returns:
-            a numpy array
+            a numpy array that may already have been cached in self.exports_dic
 
         """
         data = self.export(key, preserve_dtype)
@@ -257,6 +270,7 @@ class OTBObject(ABC):
 
         Returns:
             pixel index: (row, col)
+
         """
         spacing_x, _, origin_x, _, spacing_y, origin_y = self.transform
         row, col = (origin_y - y) / spacing_y, (x - origin_x) / spacing_x
@@ -272,8 +286,8 @@ class OTBObject(ABC):
             x: first element
             y: second element
 
-        Return:
-            operator
+        Returns:
+            an Operation object instance
 
         """
         if isinstance(y, (np.ndarray, np.generic)):
@@ -420,7 +434,6 @@ class OTBObject(ABC):
         Args:
             item: attribute name
 
-
         """
         note = (
             "Since pyotb 2.0.0, OTBObject instances have stopped to forward "
@@ -430,8 +443,6 @@ class OTBObject(ABC):
         hint = None
 
         if item in dir(self.app):
-            # Because otbApplication instances methods names start with an
-            # upper case
             hint = f"Maybe try `pyotb_app.app.{item}` instead of `pyotb_app.{item}`? "
             if item.startswith("GetParameter"):
                 hint += (
@@ -439,10 +450,8 @@ class OTBObject(ABC):
                     "shorten with `pyotb_app['paramname']` to access parameters "
                     "values."
                 )
-
         elif item in self.parameters_keys:
-            # Because in pyotb 1.5.4, applications outputs were added as
-            # attributes of the instance
+            # Because in pyotb 1.5.4, app outputs were added as instance attributes
             hint = (
                 "Note: `pyotb_app.paramname` is no longer supported. Starting "
                 "from pyotb 2.0.0, `pyotb_app['paramname']` can be used to "
@@ -469,6 +478,9 @@ class OTBObject(ABC):
         Returns:
             list of pixel values if vector image, or pixel value, or Slicer
 
+        Raises:
+            ValueError: if key is not a valid pixel index or slice
+
         """
         # Accessing pixel value(s) using Y/X coordinates
         if isinstance(key, tuple) and len(key) >= 2:
@@ -488,12 +500,15 @@ class OTBObject(ABC):
                 f'"{key}" cannot be interpreted as valid slicing. Slicing should be 2D or 3D.'
             )
         if isinstance(key, tuple) and len(key) == 2:
-            # Adding a 3rd dimension
-            key = key + (slice(None, None, None),)
+            key = key + (slice(None, None, None),)  # adding 3rd dimension
         return Slicer(self, *key)
 
     def __repr__(self) -> str:
-        """Return a string representation with object id, this is a key used to store image ref in Operation dicts."""
+        """Return a string representation with object id.
+
+        This is used as key to store image ref in Operation dicts.
+
+        """
         return f"<pyotb.{self.__class__.__name__} object, id {id(self)}>"
 
 
@@ -510,24 +525,20 @@ class App(OTBObject):
         otb.ParameterType_InputFilename,
         otb.ParameterType_InputFilenameList,
     ]
-
     OUTPUT_IMAGE_TYPES = [otb.ParameterType_OutputImage]
     OUTPUT_PARAM_TYPES = OUTPUT_IMAGE_TYPES + [
         otb.ParameterType_OutputVectorData,
         otb.ParameterType_OutputFilename,
     ]
-
-    INPUT_LIST_TYPES = [
-        otb.ParameterType_InputImageList,
-        otb.ParameterType_StringList,
-        otb.ParameterType_InputFilenameList,
-        otb.ParameterType_ListView,
-        otb.ParameterType_InputVectorDataList,
-    ]
     INPUT_IMAGES_LIST_TYPES = [
         otb.ParameterType_InputImageList,
         otb.ParameterType_InputFilenameList,
     ]
+    INPUT_LIST_TYPES = [
+        otb.ParameterType_StringList,
+        otb.ParameterType_ListView,
+        otb.ParameterType_InputVectorDataList,
+    ] + INPUT_IMAGES_LIST_TYPES
 
     def __init__(
         self,
@@ -550,7 +561,6 @@ class App(OTBObject):
             frozen: freeze OTB app in order to use execute() later and avoid blocking process during __init___
             quiet: whether to print logs of the OTB app
             name: custom name that will show up in logs, appname will be used if not provided
-
             **kwargs: used for passing application parameters.
                       e.g. il=['input1.tif', App_object2, App_object3.out], out='output.tif'
 
@@ -606,7 +616,7 @@ class App(OTBObject):
 
     @property
     def app(self) -> otb.Application:
-        """Property to return an internal _app instance."""
+        """Reference to this app otb.Application instance."""
         return self._app
 
     @property
@@ -616,41 +626,25 @@ class App(OTBObject):
 
     @property
     def exports_dic(self) -> dict[str, dict]:
-        """Returns internal _exports_dic object that contains numpy array exports."""
+        """Reference to an internal dict object that contains numpy array exports."""
         return self._exports_dic
 
     def __is_one_of_types(self, key: str, param_types: list[int]) -> bool:
-        """Helper to factor is_input and is_output."""
+        """Helper to check the type of a parameter."""
         if key not in self._all_param_types:
             raise KeyError(f"key {key} not found in the application parameters types")
         return self._all_param_types[key] in param_types
 
     def __is_multi_output(self):
-        """Check if app has multiple outputs to ensure execution during write()."""
+        """Check if app has multiple outputs to ensure re-execution during write()."""
         return len(self.outputs) > 1
 
     def is_input(self, key: str) -> bool:
-        """Returns True if the key is an input.
-
-        Args:
-            key: parameter key
-
-        Returns:
-            True if the parameter is an input, else False
-
-        """
+        """Returns True if the parameter key is an input."""
         return self.__is_one_of_types(key=key, param_types=self.INPUT_PARAM_TYPES)
 
     def is_output(self, key: str) -> bool:
-        """Returns True if the key is an output.
-
-        Args:
-            key: parameter key
-
-        Returns:
-            True if the parameter is an output, else False
-
-        """
+        """Returns True if the parameter key is an output."""
         return self.__is_one_of_types(key=key, param_types=self.OUTPUT_PARAM_TYPES)
 
     def is_key_list(self, key: str) -> bool:
@@ -670,7 +664,7 @@ class App(OTBObject):
                 if value == param_type:
                     return key
         raise TypeError(
-            f"{self.name}: could not find any parameter key matching the provided types"
+            f"{self.name}: could not find any key matching the provided types"
         )
 
     @property
@@ -705,14 +699,14 @@ class App(OTBObject):
         instead of overwriting them. Handles any parameters, i.e. in-memory & filepaths
 
         Args:
-            *args: Can be : - dictionary containing key-arguments enumeration. Useful when a key is python-reserved
-                              (e.g. "in") or contains reserved characters such as a point (e.g."mode.extent.unit")
+            *args: Can be : - dictionary containing key-arguments enumeration. Useful when a keyword is reserved (e.g. "in")
                             - string or OTBObject, useful when the user implicitly wants to set the param "in"
                             - list, useful when the user implicitly wants to set the param "il"
             **kwargs: keyword arguments e.g. il=['input1.tif', oApp_object2, App_object3.out], out='output.tif'
 
         Raises:
-            Exception: when the setting of a parameter failed
+            KeyError: when the parameter name wasn't recognized
+            RuntimeError: failed to set parameter valie
 
         """
         parameters = kwargs
@@ -723,7 +717,8 @@ class App(OTBObject):
                 key = key.replace("_", ".")
             if key not in self.parameters_keys:
                 raise KeyError(
-                    f"{self.name}: parameter '{key}' was not recognized. Available keys are {self.parameters_keys}"
+                    f"{self.name}: parameter '{key}' was not recognized."
+                    f" Available keys are {self.parameters_keys}"
                 )
             # When the parameter expects a list, if needed, change the value to list
             if self.is_key_list(key) and not isinstance(obj, (list, tuple)):
@@ -741,7 +736,8 @@ class App(OTBObject):
                 self.__set_param(key, obj)
             except (RuntimeError, TypeError, ValueError, KeyError) as e:
                 raise RuntimeError(
-                    f"{self.name}: error before execution, while setting parameter '{key}' to '{obj}': {e})"
+                    f"{self.name}: error before execution,"
+                    f" while setting '{key}' to '{obj}': {e})"
                 ) from e
             # Save / update setting value and update the Output object initialized in __init__ without a filepath
             self._settings[key] = obj
@@ -873,7 +869,6 @@ class App(OTBObject):
 
             if isinstance(ext_fname, str):
                 ext_fname = _str2dict(ext_fname)
-
             logger.debug("%s: extended filename for all outputs:", self.name)
             for key, ext in ext_fname.items():
                 logger.debug("%s: %s", key, ext)
@@ -881,14 +876,12 @@ class App(OTBObject):
             for key, filepath in kwargs.items():
                 if self._out_param_types[key] == otb.ParameterType_OutputImage:
                     new_ext_fname = ext_fname.copy()
-
-                    # grab already set extended filename key/values
+                    # Grab already set extended filename key/values
                     if "?&" in filepath:
                         filepath, already_set_ext = filepath.split("?&", 1)
-                        # extensions in filepath prevail over `new_ext_fname`
+                        # Extensions in filepath prevail over `new_ext_fname`
                         new_ext_fname.update(_str2dict(already_set_ext))
-
-                    # transform dict to str
+                    # tyransform dict to str
                     ext_fname_str = "&".join(
                         [f"{key}={value}" for key, value in new_ext_fname.items()]
                     )
@@ -911,7 +904,7 @@ class App(OTBObject):
                     key: parse_pixel_type(dtype) for key, dtype in pixel_type.items()
                 }
         elif preserve_dtype:
-            self.propagate_dtype()  # all outputs will have the same type as the main input raster
+            self.propagate_dtype()
 
         # Set parameters and flush to disk
         for key, filepath in parameters.items():
@@ -942,12 +935,11 @@ class App(OTBObject):
             )
         return bool(files) and not missing
 
-    # Private functions
     def __parse_args(self, args: list[str | OTBObject | dict | list]) -> dict[str, Any]:
         """Gather all input arguments in kwargs dict.
 
         Args:
-            args: the list of arguments passed to set_parameters()
+            args: the list of arguments passed to set_parameters (__init__ *args)
 
         Returns:
             a dictionary with the right keyword depending on the object
@@ -966,12 +958,11 @@ class App(OTBObject):
         return kwargs
 
     def __check_input_param(
-        self, obj: list | OTBObject | str | Path
+        self, obj: list | tuple | OTBObject | str | Path
     ) -> list | OTBObject | str:
         """Check the type and value of an input parameter, add vsi prefixes if needed."""
-        if isinstance(obj, list):
+        if isinstance(obj, (list, tuple)):
             return [self.__check_input_param(o) for o in obj]
-        # May be we could add some checks here
         if isinstance(obj, OTBObject):
             return obj
         if isinstance(obj, Path):
@@ -981,7 +972,6 @@ class App(OTBObject):
                 # Remote file. TODO: add support for S3 / GS / AZ
                 if obj.startswith(("https://", "http://", "ftp://")):
                     obj = "/vsicurl/" + obj
-                # Compressed file
                 prefixes = {
                     ".tar": "vsitar",
                     ".tar.gz": "vsitar",
@@ -1000,9 +990,9 @@ class App(OTBObject):
             return obj
         raise TypeError(f"{self.name}: wrong input parameter type ({type(obj)})")
 
-    def __check_output_param(self, obj: list | str | Path) -> list | str:
+    def __check_output_param(self, obj: list | tuple | str | Path) -> list | str:
         """Check the type and value of an output parameter."""
-        if isinstance(obj, list):
+        if isinstance(obj, (list, tuple)):
             return [self.__check_output_param(o) for o in obj]
         if isinstance(obj, Path):
             obj = str(obj)
@@ -1020,27 +1010,22 @@ class App(OTBObject):
         # Single-parameter cases
         if isinstance(obj, OTBObject):
             self.app.ConnectImage(key, obj.app, obj.output_image_key)
-        elif isinstance(
-            obj, otb.Application
-        ):  # this is for backward comp with plain OTB
+        elif isinstance(obj, otb.Application):
             self.app.ConnectImage(key, obj, get_out_images_param_keys(obj)[0])
-        elif (
-            key == "ram"
-        ):  # SetParameterValue in OTB<7.4 doesn't work for ram parameter cf gitlab OTB issue 2200
+        elif key == "ram":
+            # SetParameterValue in OTB<7.4 doesn't work for ram parameter cf OTB issue 2200
             self.app.SetParameterInt("ram", int(obj))
-        elif not isinstance(obj, list):  # any other parameters (str, int...)
+        # Any other parameters (str, int...)
+        elif not isinstance(obj, (list, tuple)):
             self.app.SetParameterValue(key, obj)
         # Images list
         elif self.is_key_images_list(key):
-            # To enable possible in-memory connections, we go through the list and set the parameters one by one
             for inp in obj:
                 if isinstance(inp, OTBObject):
                     self.app.ConnectImage(key, inp.app, inp.output_image_key)
-                elif isinstance(
-                    inp, otb.Application
-                ):  # this is for backward comp with plain OTB
+                elif isinstance(inp, otb.Application):
                     self.app.ConnectImage(key, obj, get_out_images_param_keys(inp)[0])
-                else:  # here `input` should be an image filepath
+                elif isinstance(inp, (str, Path)):
                     # Append `input` to the list, do not overwrite any previously set element of the image list
                     self.app.AddParameterStringList(key, inp)
                 else:
@@ -1101,7 +1086,9 @@ class App(OTBObject):
                 self.data[key] = value
 
     # Special functions
-    def __getitem__(self, key: str) -> Any | list[int | float] | int | float | Slicer:
+    def __getitem__(
+        self, key: str | tuple
+    ) -> Any | list[int | float] | int | float | Slicer:
         """This function is called when we use App()[...].
 
         We allow to return attr if key is a parameter, or call OTBObject __getitem__ for pixel values or Slicer
@@ -1141,7 +1128,10 @@ class Slicer(App):
             obj: input
             rows: slice along Y / Latitude axis
             cols: slice along X / Longitude axis
-            channels: channels, can be slicing, list or int
+            channels: bands to extract. can be slicing, list or int
+
+        Raises:
+            TypeError: if channels param isn't slice, list or int
 
         """
         super().__init__(
@@ -1157,20 +1147,18 @@ class Slicer(App):
 
         # Channel slicing
         if channels != slice(None, None, None):
-            # Trigger source app execution if needed
             nb_channels = get_nbchannels(obj)
             self.app.Execute()  # this is needed by ExtractROI for setting the `cl` parameter
-            # if needed, converting int to list
             if isinstance(channels, int):
                 channels = [channels]
-            # if needed, converting slice to list
             elif isinstance(channels, slice):
                 channels = self.channels_list_from_slice(channels)
             elif isinstance(channels, tuple):
                 channels = list(channels)
             elif not isinstance(channels, list):
-                raise ValueError(
-                    f"Invalid type for channels, should be int, slice or list of bands. : {channels}"
+                raise TypeError(
+                    f"Invalid type for channels ({type(channels)})."
+                    f" Should be int, slice or list of bands."
                 )
             # Change the potential negative index values to reverse index
             channels = [c if c >= 0 else nb_channels + c for c in channels]
@@ -1178,28 +1166,24 @@ class Slicer(App):
 
         # Spatial slicing
         spatial_slicing = False
-        # TODO: handle the step value in the slice so that NN undersampling is possible ? e.g. raster[::2, ::2]
         if rows.start is not None:
             parameters.update({"mode.extent.uly": rows.start})
             spatial_slicing = True
         if rows.stop is not None and rows.stop != -1:
-            parameters.update(
-                {"mode.extent.lry": rows.stop - 1}
-            )  # subtract 1 to respect python convention
+            # Subtract 1 to respect python convention
+            parameters.update({"mode.extent.lry": rows.stop - 1})
             spatial_slicing = True
         if cols.start is not None:
             parameters.update({"mode.extent.ulx": cols.start})
             spatial_slicing = True
         if cols.stop is not None and cols.stop != -1:
-            parameters.update(
-                {"mode.extent.lrx": cols.stop - 1}
-            )  # subtract 1 to respect python convention
+            # Subtract 1 to respect python convention
+            parameters.update({"mode.extent.lrx": cols.stop - 1})
             spatial_slicing = True
-        # These are some attributes when the user simply wants to extract *one* band to be used in an Operation
+        # When the user simply wants to extract *one* band to be used in an Operation
         if not spatial_slicing and isinstance(channels, list) and len(channels) == 1:
-            self.one_band_sliced = (
-                channels[0] + 1
-            )  # OTB convention: channels start at 1
+            # OTB convention: channels start at 1
+            self.one_band_sliced = channels[0] + 1
             self.input = obj
 
         # Execute app
@@ -1230,33 +1214,32 @@ class Operation(App):
     """
 
     def __init__(self, operator: str, *inputs, nb_bands: int = None, name: str = None):
-        """Given some inputs and an operator, this function enables to transform this into an OTB application.
+        """Given some inputs and an operator, this object enables to python operator to a BandMath operation.
 
         Operations generally involve 2 inputs (+, -...). It can have only 1 input for `abs` operator.
         It can have 3 inputs for the ternary operator `cond ? x : y`.
 
         Args:
             operator: (str) one of +, -, *, /, >, <, >=, <=, ==, !=, &, |, abs, ?
-            *inputs: inputs. Can be OTBObject, filepath, int or float
-            nb_bands: to specify the output nb of bands. Optional. Used only internally by pyotb.where
-            name: override the Operation name
+            *inputs: operands, can be OTBObject, filepath, int or float
+            nb_bands: optionally specify the output nb of bands - used only internally by pyotb.where
+            name: override the default Operation name
 
         """
         self.operator = operator
-        # We first create a 'fake' expression. E.g for the operation `input1 + input2` , we create a fake expression
-        # that is like "str(input1) + str(input2)"
+        # We first create a 'fake' expression. E.g for the operation `input1 + input2`
+        # we create a fake expression like "str(input1) + str(input2)"
         self.inputs = []
         self.nb_channels = {}
         self.fake_exp_bands = []
         self.build_fake_expressions(operator, inputs, nb_bands=nb_bands)
         # Transforming images to the adequate im#, e.g. `input1` to "im1"
-        # creating a dictionary that is like {str(input1): 'im1', 'image2.tif': 'im2', ...}.
+        # using a dictionary : {str(input1): 'im1', 'image2.tif': 'im2', ...}.
         # NB: the keys of the dictionary are strings-only, instead of 'complex' objects, to enable easy serialization
         self.im_dic = {}
         self.im_count = 1
-        map_repr_to_input = (
-            {}
-        )  # to be able to retrieve the real python object from its string representation
+        # To be able to retrieve the real python object from its string representation
+        map_repr_to_input = {}
         for inp in self.inputs:
             if not isinstance(inp, (int, float)):
                 if str(inp) not in self.im_dic:
@@ -1316,6 +1299,9 @@ class Operation(App):
             inputs: inputs. Can be OTBObject, filepath, int or float
             nb_bands: to specify the output nb of bands. Optional. Used only internally by pyotb.where
 
+        Raises:
+            ValueError: if all inputs don't have the same number of bands
+
         """
         self.inputs.clear()
         self.nb_channels.clear()
@@ -1336,30 +1322,30 @@ class Operation(App):
                 if len(inputs) == 3 and k == 0:
                     # When cond is monoband whereas the result is multiband, we expand the cond to multiband
                     cond_band = 1 if nb_bands != inp.shape[2] else band
-                    fake_exp, corresponding_inputs, nb_channels = self.make_fake_exp(
+                    fake_exp, corresp_inputs, nb_channels = self.make_fake_exp(
                         inp, cond_band, keep_logical=True
                     )
                 else:
                     # Any other input
-                    fake_exp, corresponding_inputs, nb_channels = self.make_fake_exp(
+                    fake_exp, corresp_inputs, nb_channels = self.make_fake_exp(
                         inp, band, keep_logical=False
                     )
                 expressions.append(fake_exp)
                 # Reference the inputs and nb of channels (only on first pass in the loop to avoid duplicates)
-                if i == 0 and corresponding_inputs and nb_channels:
-                    self.inputs.extend(corresponding_inputs)
+                if i == 0 and corresp_inputs and nb_channels:
+                    self.inputs.extend(corresp_inputs)
                     self.nb_channels.update(nb_channels)
 
             # Generating the fake expression of the whole operation
-            if len(inputs) == 1:  # this is only for 'abs'
+            if len(inputs) == 1:
+                # This is only for 'abs()'
                 fake_exp = f"({operator}({expressions[0]}))"
             elif len(inputs) == 2:
                 # We create here the "fake" expression. For example, for a BandMathX expression such as '2 * im1 + im2',
                 # the false expression stores the expression 2 * str(input1) + str(input2)
                 fake_exp = f"({expressions[0]} {operator} {expressions[1]})"
-            elif (
-                len(inputs) == 3 and operator == "?"
-            ):  # this is only for ternary expression
+            elif len(inputs) == 3 and operator == "?":
+                # This is only for ternary expression
                 fake_exp = f"({expressions[0]} ? {expressions[1]} : {expressions[2]})"
             self.fake_exp_bands.append(fake_exp)
 
@@ -1445,10 +1431,12 @@ class Operation(App):
 
 
 class LogicalOperation(Operation):
-    """A specialization of Operation class for boolean logical operations i.e. >, <, >=, <=, ==, !=, `&` and `|`.
+    """A specialization of Operation class for boolean logical operations.
 
-    The only difference is that not only the BandMath expression is saved (e.g. "im1b1 > 0 ? 1 : 0"), but also the
-    logical expression (e.g. "im1b1 > 0")
+    Supported operators are >, <, >=, <=, ==, !=, `&` and `|`.
+
+    The only difference is that not only the BandMath expression is saved (e.g. "im1b1 > 0 ? 1 : 0"),
+     but also the logical expression (e.g. "im1b1 > 0")
 
     """
 
@@ -1458,7 +1446,7 @@ class LogicalOperation(Operation):
         Args:
             operator: string operator (one of >, <, >=, <=, ==, !=, &, |)
             *inputs: inputs
-            nb_bands: to specify the output nb of bands. Optional. Used only internally by pyotb.where
+            nb_bands: optionally specify the output nb of bands - used only by pyotb.where
 
         """
         self.logical_fake_exp_bands = []
@@ -1481,7 +1469,7 @@ class LogicalOperation(Operation):
         Args:
             operator: str (one of >, <, >=, <=, ==, !=, &, |)
             inputs: Can be OTBObject, filepath, int or float
-            nb_bands: optionnaly specify the output nb of bands - used only internally by pyotb.where
+            nb_bands: optionally specify the output nb of bands - used only internally by pyotb.where
 
         """
         # Create a list of fake exp, each item of the list corresponding to one band
@@ -1530,7 +1518,7 @@ class Input(App):
 
 
 class Output(OTBObject):
-    """Object that behave like a pointer to a specific application output file."""
+    """Object that behave like a pointer to a specific application in-memory output or file."""
 
     _filepath: str | Path = None
 
@@ -1547,7 +1535,7 @@ class Output(OTBObject):
         Args:
             pyotb_app: The pyotb App to store reference from
             param_key: Output parameter key of the target app
-            filepath: path of the output file (if not in memory)
+            filepath: path of the output file (if not memory)
             mkdir: create missing parent directories
 
         """
@@ -1574,7 +1562,7 @@ class Output(OTBObject):
 
     @property
     def exports_dic(self) -> dict[str, dict]:
-        """Returns internal _exports_dic object that contains numpy array exports."""
+        """Reference to parent _exports_dic object that contains np array exports."""
         return self.parent_pyotb_app.exports_dic
 
     @property
@@ -1597,19 +1585,34 @@ class Output(OTBObject):
             self._filepath = path
 
     def exists(self) -> bool:
-        """Check file exist."""
+        """Check if the output file exist on disk.
+
+        Raises:
+            ValueError: if filepath is not set or is remote URL
+
+        """
         if not isinstance(self.filepath, Path):
             raise ValueError("Filepath is not set or points to a remote URL")
         return self.filepath.exists()
 
     def make_parent_dirs(self):
-        """Create missing parent directories."""
+        """Create missing parent directories.
+
+        Raises:
+            ValueError: if filepath is not set or is remote URL
+
+        """
         if not isinstance(self.filepath, Path):
             raise ValueError("Filepath is not set or points to a remote URL")
         self.filepath.parent.mkdir(parents=True, exist_ok=True)
 
     def write(self, filepath: None | str | Path = None, **kwargs) -> bool:
-        """Write output to disk, filepath is not required if it was provided to parent App during init."""
+        """Write output to disk, filepath is not required if it was provided to parent App during init.
+
+        Args:
+            filepath: path of the output file, can be None if a value was passed during app init
+
+        """
         if filepath is None:
             return self.parent_pyotb_app.write(
                 {self.output_image_key: self.filepath}, **kwargs
@@ -1630,6 +1633,9 @@ def get_nbchannels(inp: str | Path | OTBObject) -> int:
     Returns:
         number of bands in image
 
+    Raises:
+        TypeError: if inp band count cannot be retrieved
+
     """
     if isinstance(inp, OTBObject):
         return inp.shape[-1]
@@ -1657,6 +1663,9 @@ def get_pixel_type(inp: str | Path | OTBObject) -> str:
         pixel_type: OTB enum e.g. `otbApplication.ImagePixelType_uint8', which actually is an int.
                     For an OTBObject with several outputs, only the pixel type of the first output is returned
 
+    Raises:
+        TypeError: if inp pixel type cannot be retrieved
+
     """
     if isinstance(inp, OTBObject):
         return inp.app.GetParameterOutputImagePixelType(inp.output_image_key)
@@ -1679,10 +1688,14 @@ def parse_pixel_type(pixel_type: str | int) -> int:
     """Convert one str pixel type to OTB integer enum if necessary.
 
     Args:
-        pixel_type: pixel type. can be str, int or dict
+        pixel_type: pixel type. can be int or str (either OTB or numpy convention)
 
     Returns:
-        pixel_type integer value
+        pixel_type OTB enum integer value
+    
+    Raises:
+        KeyError: if pixel_type name is unknown
+        TypeError: if type(pixel_type) isn't int or str
 
     """
     if isinstance(pixel_type, int):  # normal OTB int enum
@@ -1704,19 +1717,19 @@ def parse_pixel_type(pixel_type: str | int) -> int:
         if pixel_type in datatype_to_pixeltype:
             return getattr(otb, f"ImagePixelType_{datatype_to_pixeltype[pixel_type]}")
         raise KeyError(
-            f"Unknown data type `{pixel_type}`. Available ones: {datatype_to_pixeltype}"
+            f"Unknown dtype `{pixel_type}`. Available ones: {datatype_to_pixeltype}"
         )
     raise TypeError(
         f"Bad pixel type specification ({pixel_type} of type {type(pixel_type)})"
     )
 
 
-def get_out_images_param_keys(app: OTBObject) -> list[str]:
-    """Return every output parameter keys of an OTB app."""
+def get_out_images_param_keys(otb_app: otb.Application) -> list[str]:
+    """Return every output parameter keys of a bare OTB app."""
     return [
         key
-        for key in app.GetParametersKeys()
-        if app.GetParameterType(key) == otb.ParameterType_OutputImage
+        for key in otb_app.GetParametersKeys()
+        if otb_app.GetParameterType(key) == otb.ParameterType_OutputImage
     ]
 
 
diff --git a/pyotb/depreciation.py b/pyotb/depreciation.py
index 687e6ca..6794373 100644
--- a/pyotb/depreciation.py
+++ b/pyotb/depreciation.py
@@ -1,7 +1,6 @@
 """Helps with deprecated classes and methods.
 
-Taken from https://stackoverflow.com/questions/49802412/how-to-implement-
-deprecation-in-python-with-argument-alias
+Taken from https://stackoverflow.com/questions/49802412/how-to-implement-deprecation-in-python-with-argument-alias
 """
 from typing import Callable, Dict, Any
 import functools
@@ -17,7 +16,7 @@ def depreciation_warning(message: str):
     """Shows a warning message.
 
     Args:
-        message: message
+        message: message to log
 
     """
     warnings.warn(
@@ -63,11 +62,14 @@ def rename_kwargs(func_name: str, kwargs: Dict[str, Any], aliases: Dict[str, str
         kwargs: keyword args
         aliases: aliases
 
+    Raises:
+        ValueError: if both old and new arguments are provided
+
     """
     for alias, new in aliases.items():
         if alias in kwargs:
             if new in kwargs:
-                raise TypeError(
+                raise ValueError(
                     f"{func_name} received both {alias} and {new} as arguments!"
                     f" {alias} is deprecated, use {new} instead."
                 )
diff --git a/pyotb/functions.py b/pyotb/functions.py
index ad2f24c..8fde2cd 100644
--- a/pyotb/functions.py
+++ b/pyotb/functions.py
@@ -247,7 +247,7 @@ def run_tf_function(func):
 
         """
         # Getting the string definition of the tf function (e.g. "def multiply(x1, x2):...")
-        # TODO: maybe not entirely foolproof, maybe we should use dill instead? but it would add a dependency
+        # Maybe not entirely foolproof, maybe we should use dill instead? but it would add a dependency
         func_def_str = inspect.getsource(func)
         func_name = func.__name__
 
@@ -342,7 +342,7 @@ def run_tf_function(func):
         for i, inp in enumerate(raster_inputs):
             model_serve.set_parameters({f"source{i + 1}.il": [inp]})
         model_serve.execute()
-        # TODO: handle the deletion of the temporary model ?
+        # Possible ENH: handle the deletion of the temporary model ?
 
         return model_serve
 
@@ -403,7 +403,7 @@ def define_processing_area(
         )
 
     # Handling different spatial footprints
-    # TODO: there seems to have a bug, ImageMetaData is not updated when running an app,
+    # TODO: find possible bug - ImageMetaData is not updated when running an app
     #  cf https://gitlab.orfeo-toolbox.org/orfeotoolbox/otb/-/issues/2234. Should we use ImageOrigin instead?
     if not all(
         md["UpperLeftCorner"] == any_metadata["UpperLeftCorner"]
@@ -432,10 +432,10 @@ def define_processing_area(
             lry = metadatas[reference_window_input]["LowerRightCorner"][1]
             uly = metadatas[reference_window_input]["UpperLeftCorner"][1]
         elif window_rule == "specify":
-            # TODO : when the user explicitly specifies the bounding box -> add some arguments in the function
+            # When the user explicitly specifies the bounding box -> add some arguments in the function
             ...
         elif window_rule == "union":
-            # TODO : when the user wants the final bounding box to be the union of all bounding box
+            # When the user wants the final bounding box to be the union of all bounding box
             #  It should replace any 'outside' pixel by some NoData -> add `fillvalue` argument in the function
             ...
 
@@ -458,7 +458,7 @@ def define_processing_area(
                     "mode.extent.lry": lry,
                 }
                 new_input = App("ExtractROI", params, quiet=True)
-                # TODO: OTB 7.4 fixes this bug, how to handle different versions of OTB?
+                # OTB 7.4 fixes this bug, how to handle different versions of OTB?
                 new_inputs.append(new_input)
                 # Potentially update the reference inputs for later resampling
                 if str(inp) == str(reference_pixel_size_input):
@@ -495,8 +495,9 @@ def define_processing_area(
         elif pixel_size_rule == "same_as_input":
             reference_input = reference_pixel_size_input
         elif pixel_size_rule == "specify":
-            pass
-            # TODO : when the user explicitly specify the pixel size -> add argument inside the function
+            # When the user explicitly specify the pixel size -> add argument inside the function
+            ...
+
         pixel_size = metadatas[reference_input]["GeoTransform"][1]
 
         # Perform resampling on inputs that do not comply with the target pixel size
diff --git a/pyotb/helpers.py b/pyotb/helpers.py
index e0feb63..0e6ea2a 100644
--- a/pyotb/helpers.py
+++ b/pyotb/helpers.py
@@ -60,6 +60,10 @@ def find_otb(prefix: str = OTB_ROOT, scan: bool = True):
     Returns:
         otbApplication module
 
+    Raises:
+        SystemError: is OTB is not found (when using interactive mode)
+        SystemExit: if OTB is not found, since pyotb won't be usable
+
     """
     otb = None
     # Try OTB_ROOT env variable first (allow override default OTB version)
@@ -125,6 +129,9 @@ def set_environment(prefix: str):
     Args:
         prefix: path to OTB root directory
 
+    Raises:
+        SystemError: if OTB or GDAL is not found
+
     """
     logger.info("Preparing environment for OTB in %s", prefix)
     # OTB root directory
@@ -138,16 +145,18 @@ def set_environment(prefix: str):
     lib_dir = __find_lib(prefix)
     if not lib_dir:
         raise SystemError("Can't find OTB external libraries")
-    # This does not seems to work
+    # LD library path : this does not seems to work
     if sys.platform == "linux" and built_from_source:
         new_ld_path = f"{lib_dir}:{os.environ.get('LD_LIBRARY_PATH') or ''}"
         os.environ["LD_LIBRARY_PATH"] = new_ld_path
+
     # Add python bindings directory first in PYTHONPATH
     otb_api = __find_python_api(lib_dir)
     if not otb_api:
         raise SystemError("Can't find OTB Python API")
     if otb_api not in sys.path:
         sys.path.insert(0, otb_api)
+
     # Add /bin first in PATH, in order to avoid conflicts with another GDAL install
     os.environ["PATH"] = f"{prefix / 'bin'}{os.pathsep}{os.environ['PATH']}"
     # Ensure APPLICATION_PATH is set
@@ -159,6 +168,7 @@ def set_environment(prefix: str):
     os.environ["LC_NUMERIC"] = "C"
     os.environ["GDAL_DRIVER_PATH"] = "disable"
 
+    # Find GDAL libs
     if (prefix / "share/gdal").exists():
         # Local GDAL (OTB Superbuild, .run, .exe)
         gdal_data = str(prefix / "share/gdal")
@@ -186,7 +196,7 @@ def __find_lib(prefix: str = None, otb_module=None):
         otb_module: try with otbApplication library path if found, else None
 
     Returns:
-        lib path
+        lib path, or None if not found
 
     """
     if prefix is not None:
@@ -216,7 +226,7 @@ def __find_python_api(lib_dir: Path):
         prefix: prefix
 
     Returns:
-        python API path if found, else None
+        OTB python API path, or None if not found
 
     """
     otb_api = lib_dir / "python"
@@ -235,7 +245,7 @@ def __find_apps_path(lib_dir: Path):
         lib_dir: library path
 
     Returns:
-        application path if found, else empty string
+        application path, or empty string if not found
 
     """
     if lib_dir.exists():
@@ -251,7 +261,7 @@ def __find_otb_root():
     """Search for OTB root directory in well known locations.
 
     Returns:
-        str path of the OTB directory
+        str path of the OTB directory, or None if not found
 
     """
     prefix = None
@@ -339,5 +349,5 @@ def __suggest_fix_import(error_message: str, prefix: str):
 
 
 # This part of pyotb is the first imported during __init__ and checks if OTB is found
-# If OTB is not found, a SystemExit is raised, to prevent execution of the core module
+# If OTB isn't found, a SystemExit is raised to prevent execution of the core module
 find_otb()
diff --git a/pyotb/install.py b/pyotb/install.py
index 47a6ee7..219967a 100644
--- a/pyotb/install.py
+++ b/pyotb/install.py
@@ -33,15 +33,17 @@ def otb_latest_release_tag():
     return releases[-1]
 
 
-def check_versions(sysname: str, python_minor: int, otb_major: int):
+def check_versions(sysname: str, python_minor: int, otb_major: int) -> tuple[bool, int]:
     """Verify if python version is compatible with major OTB version.
 
     Args:
         sysname: OTB's system name convention (Linux64, Darwin64, Win64)
         python_minor: minor version of python
         otb_major: major version of OTB to be installed
+
     Returns:
         (True, 0) or (False, expected_version) if case of version conflict
+
     """
     if sysname == "Win64":
         expected = 5 if otb_major in (6, 7) else 7
@@ -103,6 +105,10 @@ def install_otb(version: str = "latest", path: str = "", edit_env: bool = True):
     Returns:
         full path of the new installation
 
+    Raises:
+        SystemExit: if python version is not compatible with major OTB version
+        SystemError: if automatic env config failed
+
     """
     # Read env config
     if sys.version_info.major == 2:
@@ -120,6 +126,7 @@ def install_otb(version: str = "latest", path: str = "", edit_env: bool = True):
         raise SystemExit(
             f"Python 3.{expected} is required to import bindings on Windows."
         )
+
     # Fetch archive and run installer
     filename = f"OTB-{version}-{sysname}.{ext}"
     url = f"https://www.orfeo-toolbox.org/packages/archives/OTB/{filename}"
-- 
GitLab


From 3da1b9f61c955faa7e3a9db7fa0ac1217dc0bcd8 Mon Sep 17 00:00:00 2001
From: Vincent Delbar <vincent.delbar@latelescop.fr>
Date: Fri, 3 Nov 2023 17:14:13 +0100
Subject: [PATCH 08/30] CI: codespell typos

---
 pyotb/core.py | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/pyotb/core.py b/pyotb/core.py
index 0f27547..d6336b5 100644
--- a/pyotb/core.py
+++ b/pyotb/core.py
@@ -706,7 +706,7 @@ class App(OTBObject):
 
         Raises:
             KeyError: when the parameter name wasn't recognized
-            RuntimeError: failed to set parameter valie
+            RuntimeError: failed to set parameter value
 
         """
         parameters = kwargs
-- 
GitLab


From 0770dac0db935663b42193ed457ffb6ca97af7ab Mon Sep 17 00:00:00 2001
From: Vincent Delbar <vincent.delbar@latelescop.fr>
Date: Fri, 3 Nov 2023 17:16:32 +0100
Subject: [PATCH 09/30] ENH: use integer in check_versions

---
 pyotb/install.py | 12 ++++++------
 1 file changed, 6 insertions(+), 6 deletions(-)

diff --git a/pyotb/install.py b/pyotb/install.py
index 219967a..2d5561c 100644
--- a/pyotb/install.py
+++ b/pyotb/install.py
@@ -33,7 +33,7 @@ def otb_latest_release_tag():
     return releases[-1]
 
 
-def check_versions(sysname: str, python_minor: int, otb_major: int) -> tuple[bool, int]:
+def check_versions(sysname: str, python_minor: int, otb_major: int) -> tuple[int]:
     """Verify if python version is compatible with major OTB version.
 
     Args:
@@ -42,22 +42,22 @@ def check_versions(sysname: str, python_minor: int, otb_major: int) -> tuple[boo
         otb_major: major version of OTB to be installed
 
     Returns:
-        (True, 0) or (False, expected_version) if case of version conflict
+        (1, 0) or (0, expected_version) if case of version conflict
 
     """
     if sysname == "Win64":
         expected = 5 if otb_major in (6, 7) else 7
         if python_minor == expected:
-            return True, 0
+            return 1, 0
     elif sysname == "Darwin64":
         expected = 7, 0
         if python_minor == expected:
-            return True, 0
+            return 1, 0
     elif sysname == "Linux64":
         expected = 5 if otb_major in (6, 7) else 8
         if python_minor == expected:
-            return True, 0
-    return False, expected
+            return 1, 0
+    return 0, expected
 
 
 def env_config_unix(otb_path: Path):
-- 
GitLab


From 853b206fba8701ff304a726fc44c8de8dd761159 Mon Sep 17 00:00:00 2001
From: Vincent Delbar <vincent.delbar@latelescop.fr>
Date: Fri, 3 Nov 2023 17:28:47 +0100
Subject: [PATCH 10/30] FIX: regression in __set_param

---
 pyotb/core.py | 7 ++-----
 1 file changed, 2 insertions(+), 5 deletions(-)

diff --git a/pyotb/core.py b/pyotb/core.py
index d6336b5..80916d0 100644
--- a/pyotb/core.py
+++ b/pyotb/core.py
@@ -1034,12 +1034,9 @@ class App(OTBObject):
                         f" found in '{key}' list: {inp}"
                     )
         # List of any other types (str, int...)
-        elif self.is_key_list(key):
-            self.app.SetParameterValue(key, obj)
         else:
-            raise TypeError(
-                f"{self.name}: wrong input parameter type ({type(obj)}) for '{key}'"
-            )
+            # TODO: use self.is_key_list, but this is not working for ExtractROI param "cl" which is ParameterType_UNKNOWN
+            self.app.SetParameterValue(key, obj)
 
     def __sync_parameters(self):
         """Save app parameters in _auto_parameters or data dict.
-- 
GitLab


From e1791ea4b0ceb3069ae6012a11b8c9842f70a944 Mon Sep 17 00:00:00 2001
From: Vincent Delbar <vincent.delbar@latelescop.fr>
Date: Fri, 3 Nov 2023 18:06:47 +0100
Subject: [PATCH 11/30] BUG: add missing annotations import from __future__

---
 pyotb/install.py | 2 ++
 1 file changed, 2 insertions(+)

diff --git a/pyotb/install.py b/pyotb/install.py
index 2d5561c..5a8256a 100644
--- a/pyotb/install.py
+++ b/pyotb/install.py
@@ -1,4 +1,6 @@
 """This module contains functions for interactive auto installation of OTB."""
+from __future__ import annotations
+
 import json
 import os
 import re
-- 
GitLab


From d09c718ad945fc0a0632a1248cd28fef9fe415c2 Mon Sep 17 00:00:00 2001
From: Vincent Delbar <vincent.delbar@latelescop.fr>
Date: Fri, 3 Nov 2023 18:07:39 +0100
Subject: [PATCH 12/30] DOC: remove useless comment since bug is fixed

---
 pyotb/functions.py | 1 -
 1 file changed, 1 deletion(-)

diff --git a/pyotb/functions.py b/pyotb/functions.py
index 8fde2cd..fe93641 100644
--- a/pyotb/functions.py
+++ b/pyotb/functions.py
@@ -458,7 +458,6 @@ def define_processing_area(
                     "mode.extent.lry": lry,
                 }
                 new_input = App("ExtractROI", params, quiet=True)
-                # OTB 7.4 fixes this bug, how to handle different versions of OTB?
                 new_inputs.append(new_input)
                 # Potentially update the reference inputs for later resampling
                 if str(inp) == str(reference_pixel_size_input):
-- 
GitLab


From 5c8f8dcccc73ddbbe1042208488f299aea44c03f Mon Sep 17 00:00:00 2001
From: Vincent Delbar <vincent.delbar@latelescop.fr>
Date: Fri, 3 Nov 2023 18:52:24 +0100
Subject: [PATCH 13/30] ENH: back to bool since type hints bug is fixed

---
 pyotb/install.py | 12 ++++++------
 1 file changed, 6 insertions(+), 6 deletions(-)

diff --git a/pyotb/install.py b/pyotb/install.py
index 5a8256a..0cf139e 100644
--- a/pyotb/install.py
+++ b/pyotb/install.py
@@ -35,7 +35,7 @@ def otb_latest_release_tag():
     return releases[-1]
 
 
-def check_versions(sysname: str, python_minor: int, otb_major: int) -> tuple[int]:
+def check_versions(sysname: str, python_minor: int, otb_major: int) -> tuple[bool, int]:
     """Verify if python version is compatible with major OTB version.
 
     Args:
@@ -44,22 +44,22 @@ def check_versions(sysname: str, python_minor: int, otb_major: int) -> tuple[int
         otb_major: major version of OTB to be installed
 
     Returns:
-        (1, 0) or (0, expected_version) if case of version conflict
+        (True, 0) if compatible or (False, expected_version) in case of conflicts
 
     """
     if sysname == "Win64":
         expected = 5 if otb_major in (6, 7) else 7
         if python_minor == expected:
-            return 1, 0
+            return True, 0
     elif sysname == "Darwin64":
         expected = 7, 0
         if python_minor == expected:
-            return 1, 0
+            return True, 0
     elif sysname == "Linux64":
         expected = 5 if otb_major in (6, 7) else 8
         if python_minor == expected:
-            return 1, 0
-    return 0, expected
+            return True, 0
+    return False, expected
 
 
 def env_config_unix(otb_path: Path):
-- 
GitLab


From 014a07574aa05a0cffb7c573564b5815e77ef0a8 Mon Sep 17 00:00:00 2001
From: Vincent Delbar <vincent.delbar@latelescop.fr>
Date: Fri, 3 Nov 2023 19:35:57 +0100
Subject: [PATCH 14/30] ENH: remove useless exceptions since already checked in
 set_parameters

---
 pyotb/core.py | 7 +------
 1 file changed, 1 insertion(+), 6 deletions(-)

diff --git a/pyotb/core.py b/pyotb/core.py
index 80916d0..df3297a 100644
--- a/pyotb/core.py
+++ b/pyotb/core.py
@@ -1025,14 +1025,9 @@ class App(OTBObject):
                     self.app.ConnectImage(key, inp.app, inp.output_image_key)
                 elif isinstance(inp, otb.Application):
                     self.app.ConnectImage(key, obj, get_out_images_param_keys(inp)[0])
-                elif isinstance(inp, (str, Path)):
+                else:
                     # Append `input` to the list, do not overwrite any previously set element of the image list
                     self.app.AddParameterStringList(key, inp)
-                else:
-                    raise TypeError(
-                        f"{self.name}: wrong input parameter type ({type(inp)})"
-                        f" found in '{key}' list: {inp}"
-                    )
         # List of any other types (str, int...)
         else:
             # TODO: use self.is_key_list, but this is not working for ExtractROI param "cl" which is ParameterType_UNKNOWN
-- 
GitLab


From 848f4e4aab6d81ad773f81fcf15cef9d2e23fb87 Mon Sep 17 00:00:00 2001
From: Vincent Delbar <vincent.delbar@latelescop.fr>
Date: Mon, 6 Nov 2023 14:15:31 +0100
Subject: [PATCH 15/30] ENH: add exception, use if self.is_key_list(key) in
 __set_param

---
 pyotb/core.py | 15 ++++++++++-----
 1 file changed, 10 insertions(+), 5 deletions(-)

diff --git a/pyotb/core.py b/pyotb/core.py
index df3297a..c0a824b 100644
--- a/pyotb/core.py
+++ b/pyotb/core.py
@@ -534,11 +534,12 @@ class App(OTBObject):
         otb.ParameterType_InputImageList,
         otb.ParameterType_InputFilenameList,
     ]
-    INPUT_LIST_TYPES = [
+    INPUT_LIST_TYPES = INPUT_IMAGES_LIST_TYPES + [
         otb.ParameterType_StringList,
         otb.ParameterType_ListView,
         otb.ParameterType_InputVectorDataList,
-    ] + INPUT_IMAGES_LIST_TYPES
+        otb.ParameterType_Band,
+    ]
 
     def __init__(
         self,
@@ -1025,13 +1026,17 @@ class App(OTBObject):
                     self.app.ConnectImage(key, inp.app, inp.output_image_key)
                 elif isinstance(inp, otb.Application):
                     self.app.ConnectImage(key, obj, get_out_images_param_keys(inp)[0])
+                # Here inp is either str or Path, already checked by __check_*_param
                 else:
-                    # Append `input` to the list, do not overwrite any previously set element of the image list
+                    # Append it to the list, do not overwrite any previously set element of the image list
                     self.app.AddParameterStringList(key, inp)
         # List of any other types (str, int...)
-        else:
-            # TODO: use self.is_key_list, but this is not working for ExtractROI param "cl" which is ParameterType_UNKNOWN
+        elif self.is_key_list(key):
             self.app.SetParameterValue(key, obj)
+        else:
+            raise TypeError(
+                f"{self.name}: wrong parameter type ({type(obj)}) for '{key}'"
+            )
 
     def __sync_parameters(self):
         """Save app parameters in _auto_parameters or data dict.
-- 
GitLab


From 8b257b5073c5ce5ce142a73eff8e3f32cb569c14 Mon Sep 17 00:00:00 2001
From: Vincent Delbar <vincent.delbar@latelescop.fr>
Date: Mon, 6 Nov 2023 14:40:24 +0100
Subject: [PATCH 16/30] ENH: type hints (float | int) -> (float)

---
 pyotb/core.py | 40 ++++++++++++++++++++--------------------
 1 file changed, 20 insertions(+), 20 deletions(-)

diff --git a/pyotb/core.py b/pyotb/core.py
index c0a824b..f03e9e1 100644
--- a/pyotb/core.py
+++ b/pyotb/core.py
@@ -294,35 +294,35 @@ class OTBObject(ABC):
             return NotImplemented  # this enables to fallback on numpy emulation thanks to __array_ufunc__
         return op_cls(name, x, y)
 
-    def __add__(self, other: OTBObject | str | int | float) -> Operation:
+    def __add__(self, other: OTBObject | str | float) -> Operation:
         """Addition."""
         return self.__create_operator(Operation, "+", self, other)
 
-    def __sub__(self, other: OTBObject | str | int | float) -> Operation:
+    def __sub__(self, other: OTBObject | str | float) -> Operation:
         """Subtraction."""
         return self.__create_operator(Operation, "-", self, other)
 
-    def __mul__(self, other: OTBObject | str | int | float) -> Operation:
+    def __mul__(self, other: OTBObject | str | float) -> Operation:
         """Multiplication."""
         return self.__create_operator(Operation, "*", self, other)
 
-    def __truediv__(self, other: OTBObject | str | int | float) -> Operation:
+    def __truediv__(self, other: OTBObject | str | float) -> Operation:
         """Division."""
         return self.__create_operator(Operation, "/", self, other)
 
-    def __radd__(self, other: OTBObject | str | int | float) -> Operation:
+    def __radd__(self, other: OTBObject | str | float) -> Operation:
         """Right addition."""
         return self.__create_operator(Operation, "+", other, self)
 
-    def __rsub__(self, other: OTBObject | str | int | float) -> Operation:
+    def __rsub__(self, other: OTBObject | str | float) -> Operation:
         """Right subtraction."""
         return self.__create_operator(Operation, "-", other, self)
 
-    def __rmul__(self, other: OTBObject | str | int | float) -> Operation:
+    def __rmul__(self, other: OTBObject | str | float) -> Operation:
         """Right multiplication."""
         return self.__create_operator(Operation, "*", other, self)
 
-    def __rtruediv__(self, other: OTBObject | str | int | float) -> Operation:
+    def __rtruediv__(self, other: OTBObject | str | float) -> Operation:
         """Right division."""
         return self.__create_operator(Operation, "/", other, self)
 
@@ -330,35 +330,35 @@ class OTBObject(ABC):
         """Absolute value."""
         return Operation("abs", self)
 
-    def __ge__(self, other: OTBObject | str | int | float) -> LogicalOperation:
+    def __ge__(self, other: OTBObject | str | float) -> LogicalOperation:
         """Greater of equal than."""
         return self.__create_operator(LogicalOperation, ">=", self, other)
 
-    def __le__(self, other: OTBObject | str | int | float) -> LogicalOperation:
+    def __le__(self, other: OTBObject | str | float) -> LogicalOperation:
         """Lower of equal than."""
         return self.__create_operator(LogicalOperation, "<=", self, other)
 
-    def __gt__(self, other: OTBObject | str | int | float) -> LogicalOperation:
+    def __gt__(self, other: OTBObject | str | float) -> LogicalOperation:
         """Greater than."""
         return self.__create_operator(LogicalOperation, ">", self, other)
 
-    def __lt__(self, other: OTBObject | str | int | float) -> LogicalOperation:
+    def __lt__(self, other: OTBObject | str | float) -> LogicalOperation:
         """Lower than."""
         return self.__create_operator(LogicalOperation, "<", self, other)
 
-    def __eq__(self, other: OTBObject | str | int | float) -> LogicalOperation:
+    def __eq__(self, other: OTBObject | str | float) -> LogicalOperation:
         """Equality."""
         return self.__create_operator(LogicalOperation, "==", self, other)
 
-    def __ne__(self, other: OTBObject | str | int | float) -> LogicalOperation:
+    def __ne__(self, other: OTBObject | str | float) -> LogicalOperation:
         """Inequality."""
         return self.__create_operator(LogicalOperation, "!=", self, other)
 
-    def __or__(self, other: OTBObject | str | int | float) -> LogicalOperation:
+    def __or__(self, other: OTBObject | str | float) -> LogicalOperation:
         """Logical or."""
         return self.__create_operator(LogicalOperation, "||", self, other)
 
-    def __and__(self, other: OTBObject | str | int | float) -> LogicalOperation:
+    def __and__(self, other: OTBObject | str | float) -> LogicalOperation:
         """Logical and."""
         return self.__create_operator(LogicalOperation, "&&", self, other)
 
@@ -1085,7 +1085,7 @@ class App(OTBObject):
     # Special functions
     def __getitem__(
         self, key: str | tuple
-    ) -> Any | list[int | float] | int | float | Slicer:
+    ) -> Any | list[float] | float | Slicer:
         """This function is called when we use App()[...].
 
         We allow to return attr if key is a parameter, or call OTBObject __getitem__ for pixel values or Slicer
@@ -1255,7 +1255,7 @@ class Operation(App):
             appname, il=self.unique_inputs, exp=self.exp, quiet=True, name=name
         )
 
-    def get_nb_bands(self, inputs: list[OTBObject | str | int | float]) -> int:
+    def get_nb_bands(self, inputs: list[OTBObject | str | float]) -> int:
         """Guess the number of bands of the output image, from the inputs.
 
         Args:
@@ -1284,7 +1284,7 @@ class Operation(App):
     def build_fake_expressions(
         self,
         operator: str,
-        inputs: list[OTBObject | str | int | float],
+        inputs: list[OTBObject | str | float],
         nb_bands: int = None,
     ):
         """Create a list of 'fake' expressions, one for each band.
@@ -1455,7 +1455,7 @@ class LogicalOperation(Operation):
     def build_fake_expressions(
         self,
         operator: str,
-        inputs: list[OTBObject | str | int | float],
+        inputs: list[OTBObject | str | float],
         nb_bands: int = None,
     ):
         """Create a list of 'fake' expressions, one for each band.
-- 
GitLab


From d4384d14129d1c5843e8bb191a52bb24a879b6e1 Mon Sep 17 00:00:00 2001
From: Vincent Delbar <vincent.delbar@latelescop.fr>
Date: Mon, 6 Nov 2023 15:06:48 +0100
Subject: [PATCH 17/30] ENH: summarize comments

---
 pyotb/core.py | 3 ++-
 1 file changed, 2 insertions(+), 1 deletion(-)

diff --git a/pyotb/core.py b/pyotb/core.py
index f03e9e1..b614a4d 100644
--- a/pyotb/core.py
+++ b/pyotb/core.py
@@ -1758,6 +1758,7 @@ def summarize(
         return [summarize(o) for o in obj]
     if isinstance(obj, Output):
         return summarize(obj.parent_pyotb_app)
+    # => This is the deepest recursion level
     if not isinstance(obj, App):
         return obj
 
@@ -1769,8 +1770,8 @@ def summarize(
         return param.split("?")[0]
 
     # Call / top level of recursion : obj is an App
-    # We need to return parameters values, summarized if param is an App
     parameters = {}
+    # We need to return parameters values, summarized if param is an App
     for key, param in obj.parameters.items():
         if strip_inpath and obj.is_input(key) or strip_outpath and obj.is_output(key):
             parameters[key] = strip_path(param)
-- 
GitLab


From b219d44312a63d0e01372f3d1310c5a4f40e2d9b Mon Sep 17 00:00:00 2001
From: Vincent Delbar <vincent.delbar@latelescop.fr>
Date: Mon, 6 Nov 2023 15:06:50 +0100
Subject: [PATCH 18/30] DOC: avoid multiline argument text in docstrings

---
 pyotb/core.py | 104 +++++++++++++++++++++++++-------------------------
 1 file changed, 51 insertions(+), 53 deletions(-)

diff --git a/pyotb/core.py b/pyotb/core.py
index b614a4d..5528039 100644
--- a/pyotb/core.py
+++ b/pyotb/core.py
@@ -136,14 +136,14 @@ class OTBObject(ABC):
         return App("ComputeImagesStatistics", self, quiet=True).data
 
     def get_values_at_coords(
-        self, row: int, col: int, bands: int = None
+        self, row: int, col: int, bands: int | list[int] = None
     ) -> list[float] | float:
         """Get pixel value(s) at a given YX coordinates.
 
         Args:
             row: index along Y / latitude axis
             col: index along X / longitude axis
-            bands: band number, list or slice to fetch values from
+            bands: band number(s) to fetch values from
 
         Returns:
             single numerical value or a list of values for each band
@@ -209,8 +209,7 @@ class OTBObject(ABC):
 
         Args:
             key: parameter key to export, if None then the default one will be used
-            preserve_dtype: when set to True, the numpy array is converted to the same pixel type as
-                            the App first output. Default is True
+            preserve_dtype: convert the array to the same pixel type as the App first output
 
         Returns:
             the exported numpy array
@@ -231,12 +230,13 @@ class OTBObject(ABC):
     ) -> np.ndarray:
         """Export a pyotb object to numpy array.
 
+        A copy is avoided by default, but may be required if preserve_dtype is False
+         and the source app reference is lost.
+
         Args:
             key: the output parameter name to export as numpy array
-            preserve_dtype: when set to True, the numpy array is converted to the same pixel type as
-                            the App first output. Default is True
-            copy: whether to copy the output array, default is False
-                  required to True if preserve_dtype is False and the source app reference is lost
+            preserve_dtype:  convert the array to the same pixel type as the App first output
+            copy: whether to copy the output array instead of returning a reference
 
         Returns:
             a numpy array that may already have been cached in self.exports_dic
@@ -378,12 +378,12 @@ class OTBObject(ABC):
         """This is called whenever a numpy function is called on a pyotb object.
 
         Operation is performed in numpy, then imported back to pyotb with the same georeference as input.
+        At least one obj is unputs has to be an OTBObject.
 
         Args:
             ufunc: numpy function
             method: an internal numpy argument
-            inputs: inputs, at least one being pyotb object. If there are several pyotb objects, they must all have
-                    the same georeference and pixel size.
+            inputs: inputs, with equal shape in case of several images / OTBObject
             **kwargs: kwargs of the numpy function
 
         Returns:
@@ -552,18 +552,19 @@ class App(OTBObject):
     ):
         """Common constructor for OTB applications. Handles in-memory connection between apps.
 
+        There are several ways to pass parameters to init the app. *args can be :
+            - dictionary containing key-arguments enumeration. Useful when a key is python-reserved
+                (e.g. "in") or contains reserved characters such as a point (e.g."mode.extent.unit")
+            - string, App or Output, useful when the user wants to specify the input "in"
+            - list, useful when the user wants to specify the input list 'il'
+
         Args:
             appname: name of the OTB application to initialize, e.g. 'BandMath'
-            *args: used for passing application parameters. Can be :
-                           - dictionary containing key-arguments enumeration. Useful when a key is python-reserved
-                             (e.g. "in") or contains reserved characters such as a point (e.g."mode.extent.unit")
-                           - string, App or Output, useful when the user wants to specify the input "in"
-                           - list, useful when the user wants to specify the input list 'il'
+            *args: used to pass an app input as argument and ommiting the key
             frozen: freeze OTB app in order to use execute() later and avoid blocking process during __init___
             quiet: whether to print logs of the OTB app
             name: custom name that will show up in logs, appname will be used if not provided
-            **kwargs: used for passing application parameters.
-                      e.g. il=['input1.tif', App_object2, App_object3.out], out='output.tif'
+            **kwargs: used for passing application parameters (e.g. il=["image_1.tif", "image_1.tif"])
 
         """
         # Attributes and data structures used by properties
@@ -700,10 +701,8 @@ class App(OTBObject):
         instead of overwriting them. Handles any parameters, i.e. in-memory & filepaths
 
         Args:
-            *args: Can be : - dictionary containing key-arguments enumeration. Useful when a keyword is reserved (e.g. "in")
-                            - string or OTBObject, useful when the user implicitly wants to set the param "in"
-                            - list, useful when the user implicitly wants to set the param "il"
-            **kwargs: keyword arguments e.g. il=['input1.tif', oApp_object2, App_object3.out], out='output.tif'
+            *args: any input OTBObject, filepath or images list, or a dict of parameters
+            **kwargs: app parameters, with "_" instead of dots e.g. io_in="image.tif"
 
         Raises:
             KeyError: when the parameter name wasn't recognized
@@ -818,20 +817,19 @@ class App(OTBObject):
     ) -> bool:
         """Set output pixel type and write the output raster files.
 
+        The first argument is expected to be:
+            - filepath, useful when there is only one output, e.g. 'output.tif'
+            - dictionary containing output filepath
+            - None if output file was passed during App init
+        In case of multiple outputs, pixel_type may also be a dictionary with parameter names as keys.
+        Accepted pixel types : uint8, uint16, uint32, int16, int32, float, double, cint16, cint32, cfloat, cdouble
+
         Args:
-            path: Can be : - filepath, useful when there is only one output, e.g. 'output.tif'
-                           - dictionary containing key-arguments enumeration. Useful when a key contains
-                             non-standard characters such as a point, e.g. {'io.out':'output.tif'}
-                           - None if output file was passed during App init
-            pixel_type: Can be : - dictionary {out_param_key: pixeltype} when specifying for several outputs
-                                 - str (e.g. 'uint16') or otbApplication.ImagePixelType_... When there are several
-                                   outputs, all outputs are written with this unique type.
-                                   Valid pixel types are uint8, uint16, uint32, int16, int32, float, double,
-                                   cint16, cint32, cfloat, cdouble. (Default value = None)
+            path: output filepath or dict of filepath with param keys
+            pixel_type: pixel type string representation
             preserve_dtype: propagate main input pixel type to outputs, in case pixel_type is None
-            ext_fname: Optional, an extended filename as understood by OTB (e.g. "&gdal:co:TILED=YES")
-                                Will be used for all outputs (Default value = None)
-            **kwargs: keyword arguments e.g. out='output.tif'
+            ext_fname: an OTB extended filename, will be applied to every output (but won't overwrite existing keys in output filepath)
+            **kwargs: keyword arguments e.g. out='output.tif' or io_out='output.tif'
 
         Returns:
             True if all files are found on disk
@@ -1125,7 +1123,7 @@ class Slicer(App):
             obj: input
             rows: slice along Y / Latitude axis
             cols: slice along X / Longitude axis
-            channels: bands to extract. can be slicing, list or int
+            channels: bands to extract
 
         Raises:
             TypeError: if channels param isn't slice, list or int
@@ -1218,7 +1216,7 @@ class Operation(App):
 
         Args:
             operator: (str) one of +, -, *, /, >, <, >=, <=, ==, !=, &, |, abs, ?
-            *inputs: operands, can be OTBObject, filepath, int or float
+            *inputs: operands of the expression to build
             nb_bands: optionally specify the output nb of bands - used only internally by pyotb.where
             name: override the default Operation name
 
@@ -1292,7 +1290,7 @@ class Operation(App):
         E.g for the operation input1 + input2, we create a fake expression that is like "str(input1) + str(input2)"
 
         Args:
-            operator: (str) one of +, -, *, /, >, <, >=, <=, ==, !=, &, |, abs, ?
+            operator: one of +, -, *, /, >, <, >=, <=, ==, !=, &, |, abs, ?
             inputs: inputs. Can be OTBObject, filepath, int or float
             nb_bands: to specify the output nb of bands. Optional. Used only internally by pyotb.where
 
@@ -1375,14 +1373,14 @@ class Operation(App):
         """This an internal function, only to be used by `build_fake_expressions`.
 
         Enable to create a fake expression just for one input and one band.
+        Regarding the "keep_logical" param: 
+            - if True, for `input1 > input2`, returned fake expression is "str(input1) > str(input2)"
+            - if False, for `input1 > input2`, returned fake exp is "str(input1) > str(input2) ? 1 : 0"]  Default False
 
         Args:
             x: input
             band: which band to consider (bands start at 1)
             keep_logical: whether to keep the logical expressions "as is" in case the input is a logical operation.
-                          ex: if True, for `input1 > input2`, returned fake expression is "str(input1) > str(input2)"
-                          if False, for `input1 > input2`, returned fake exp is "str(input1) > str(input2) ? 1 : 0"]
-                          Default False
 
         Returns:
             fake_exp: the fake expression for this band and input
@@ -1498,7 +1496,7 @@ class Input(App):
         """Default constructor.
 
         Args:
-            filepath: Anything supported by GDAL (local file on the filesystem, remote resource e.g. /vsicurl/.., etc.)
+            filepath: Anything supported by GDAL (local file on the filesystem, remote resource, etc.)
 
         """
         super().__init__("ExtractROI", {"in": filepath}, quiet=True, frozen=True)
@@ -1625,7 +1623,7 @@ def get_nbchannels(inp: str | Path | OTBObject) -> int:
     """Get the nb of bands of input image.
 
     Args:
-        inp: can be filepath or OTBObject object
+        inp: input file or OTBObject
 
     Returns:
         number of bands in image
@@ -1651,14 +1649,16 @@ def get_nbchannels(inp: str | Path | OTBObject) -> int:
 
 
 def get_pixel_type(inp: str | Path | OTBObject) -> str:
-    """Get the encoding of input image pixels.
+    """Get the encoding of input image pixels as integer enum.
+
+    OTB enum e.g. `otbApplication.ImagePixelType_uint8'.
+    For an OTBObject with several outputs, only the pixel type of the first output is returned
 
     Args:
-        inp: can be filepath or pyotb object
+        inp: input file or OTBObject
 
     Returns:
-        pixel_type: OTB enum e.g. `otbApplication.ImagePixelType_uint8', which actually is an int.
-                    For an OTBObject with several outputs, only the pixel type of the first output is returned
+        OTB enum
 
     Raises:
         TypeError: if inp pixel type cannot be retrieved
@@ -1685,7 +1685,7 @@ def parse_pixel_type(pixel_type: str | int) -> int:
     """Convert one str pixel type to OTB integer enum if necessary.
 
     Args:
-        pixel_type: pixel type. can be int or str (either OTB or numpy convention)
+        pixel_type: pixel type to parse
 
     Returns:
         pixel_type OTB enum integer value
@@ -1739,21 +1739,19 @@ def summarize(
 
     At the deepest recursion level, this function just return any parameter value,
      path stripped if needed, or app summarized in case of a pipeline.
+    If strip_path is enabled, paths are truncated after the first "?" character.
+    Can be useful to remove URLs tokens from inputs (e.g. SAS or S3 credentials),
+     or extended filenames from outputs.
 
     Args:
         obj: input object / parameter value to summarize
-        strip_inpath: strip all input paths: If enabled, paths related to
-            inputs are truncated after the first "?" character. Can be useful
-            to remove URLs tokens (e.g. SAS or S3 credentials).
-        strip_outpath: strip all output paths: If enabled, paths related
-            to outputs are truncated after the first "?" character. Can be
-            useful to remove extended filenames.
+        strip_inpath: strip all input paths
+        strip_outpath: strip all output paths
 
     Returns:
         nested dictionary containing name and parameters of an app and its parents
 
     """
-    # This is the deepest recursion level
     if isinstance(obj, list):
         return [summarize(o) for o in obj]
     if isinstance(obj, Output):
-- 
GitLab


From d729f02568fc4949538be2b413630efc418c0976 Mon Sep 17 00:00:00 2001
From: Vincent Delbar <vincent.delbar@latelescop.fr>
Date: Mon, 6 Nov 2023 15:11:40 +0100
Subject: [PATCH 19/30] STYLE: apply black

---
 pyotb/core.py | 12 ++++--------
 1 file changed, 4 insertions(+), 8 deletions(-)

diff --git a/pyotb/core.py b/pyotb/core.py
index 5528039..5c33706 100644
--- a/pyotb/core.py
+++ b/pyotb/core.py
@@ -1081,9 +1081,7 @@ class App(OTBObject):
                 self.data[key] = value
 
     # Special functions
-    def __getitem__(
-        self, key: str | tuple
-    ) -> Any | list[float] | float | Slicer:
+    def __getitem__(self, key: str | tuple) -> Any | list[float] | float | Slicer:
         """This function is called when we use App()[...].
 
         We allow to return attr if key is a parameter, or call OTBObject __getitem__ for pixel values or Slicer
@@ -1270,9 +1268,7 @@ class Operation(App):
             return 1
         # Check that all inputs have the same band count
         nb_bands_list = [
-            get_nbchannels(inp)
-            for inp in inputs
-            if not isinstance(inp, (float, int))
+            get_nbchannels(inp) for inp in inputs if not isinstance(inp, (float, int))
         ]
         all_same = all(x == nb_bands_list[0] for x in nb_bands_list)
         if len(nb_bands_list) > 1 and not all_same:
@@ -1373,7 +1369,7 @@ class Operation(App):
         """This an internal function, only to be used by `build_fake_expressions`.
 
         Enable to create a fake expression just for one input and one band.
-        Regarding the "keep_logical" param: 
+        Regarding the "keep_logical" param:
             - if True, for `input1 > input2`, returned fake expression is "str(input1) > str(input2)"
             - if False, for `input1 > input2`, returned fake exp is "str(input1) > str(input2) ? 1 : 0"]  Default False
 
@@ -1689,7 +1685,7 @@ def parse_pixel_type(pixel_type: str | int) -> int:
 
     Returns:
         pixel_type OTB enum integer value
-    
+
     Raises:
         KeyError: if pixel_type name is unknown
         TypeError: if type(pixel_type) isn't int or str
-- 
GitLab


From 09bfa60e027fb6bc7fd6b036e2d97649e4eb5952 Mon Sep 17 00:00:00 2001
From: Vincent Delbar <vincent.delbar@latelescop.fr>
Date: Mon, 6 Nov 2023 15:17:17 +0100
Subject: [PATCH 20/30] DOC: typo

---
 pyotb/core.py | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/pyotb/core.py b/pyotb/core.py
index 5c33706..b6655f3 100644
--- a/pyotb/core.py
+++ b/pyotb/core.py
@@ -560,7 +560,7 @@ class App(OTBObject):
 
         Args:
             appname: name of the OTB application to initialize, e.g. 'BandMath'
-            *args: used to pass an app input as argument and ommiting the key
+            *args: used to pass an app input as argument and omitting the key
             frozen: freeze OTB app in order to use execute() later and avoid blocking process during __init___
             quiet: whether to print logs of the OTB app
             name: custom name that will show up in logs, appname will be used if not provided
-- 
GitLab


From 3ee79f5672397fd4d985a7c43f96d9c6a0182482 Mon Sep 17 00:00:00 2001
From: Vincent Delbar <vincent.delbar@latelescop.fr>
Date: Mon, 6 Nov 2023 15:38:43 +0100
Subject: [PATCH 21/30] DOC: test move docs from __init__ to class docstring

---
 pyotb/core.py | 37 +++++++++++++++++++------------------
 1 file changed, 19 insertions(+), 18 deletions(-)

diff --git a/pyotb/core.py b/pyotb/core.py
index b6655f3..f578a66 100644
--- a/pyotb/core.py
+++ b/pyotb/core.py
@@ -513,7 +513,24 @@ class OTBObject(ABC):
 
 
 class App(OTBObject):
-    """Base class that gathers common operations for any OTB application."""
+    """Wrapper around otb.Application to handle settings and execution.
+
+    Base class that gathers common operations for any OTB application lifetime (settings, exec, export, etc.)
+    There are several ways to pass parameters to init the app. *args can be :
+        - dictionary containing key-arguments enumeration. Useful when a key is python-reserved
+            (e.g. "in") or contains reserved characters such as a point (e.g."mode.extent.unit")
+        - string, App or Output, useful when the user wants to specify the input "in"
+        - list, useful when the user wants to specify the input list 'il'
+
+    Args:
+        appname: name of the OTB application to initialize, e.g. 'BandMath'
+        *args: used to pass an app input as argument and omitting the key
+        frozen: freeze OTB app in order avoid blocking during __init___
+        quiet: whether to print logs of the OTB app
+        name: custom name that will show up in logs, appname will be used if not provided
+        **kwargs: used for passing application parameters (e.g. il=["image_1.tif", "image_1.tif"])
+
+    """
 
     INPUT_IMAGE_TYPES = [
         otb.ParameterType_InputImage,
@@ -550,23 +567,7 @@ class App(OTBObject):
         name: str = "",
         **kwargs,
     ):
-        """Common constructor for OTB applications. Handles in-memory connection between apps.
-
-        There are several ways to pass parameters to init the app. *args can be :
-            - dictionary containing key-arguments enumeration. Useful when a key is python-reserved
-                (e.g. "in") or contains reserved characters such as a point (e.g."mode.extent.unit")
-            - string, App or Output, useful when the user wants to specify the input "in"
-            - list, useful when the user wants to specify the input list 'il'
-
-        Args:
-            appname: name of the OTB application to initialize, e.g. 'BandMath'
-            *args: used to pass an app input as argument and omitting the key
-            frozen: freeze OTB app in order to use execute() later and avoid blocking process during __init___
-            quiet: whether to print logs of the OTB app
-            name: custom name that will show up in logs, appname will be used if not provided
-            **kwargs: used for passing application parameters (e.g. il=["image_1.tif", "image_1.tif"])
-
-        """
+        """Common constructor for OTB applications, automatically handles in-memory connections."""
         # Attributes and data structures used by properties
         create = (
             otb.Registry.CreateApplicationWithoutLogger
-- 
GitLab


From d44ffae4691678b535c067effa31a3d4677e5371 Mon Sep 17 00:00:00 2001
From: Vincent Delbar <vincent.delbar@latelescop.fr>
Date: Mon, 6 Nov 2023 15:51:57 +0100
Subject: [PATCH 22/30] ENH: App docstring

---
 pyotb/core.py | 16 ++++++++--------
 1 file changed, 8 insertions(+), 8 deletions(-)

diff --git a/pyotb/core.py b/pyotb/core.py
index f578a66..d6a60d6 100644
--- a/pyotb/core.py
+++ b/pyotb/core.py
@@ -16,7 +16,7 @@ from .depreciation import deprecated_alias, depreciation_warning, deprecated_att
 
 
 class OTBObject(ABC):
-    """Abstraction of an image object."""
+    """Abstraction of an image object, for a whole app or one specific output."""
 
     @property
     @abstractmethod
@@ -269,7 +269,7 @@ class OTBObject(ABC):
             y: latitude or projected Y
 
         Returns:
-            pixel index: (row, col)
+            pixel index as (row, col)
 
         """
         spacing_x, _, origin_x, _, spacing_y, origin_y = self.transform
@@ -516,17 +516,17 @@ class App(OTBObject):
     """Wrapper around otb.Application to handle settings and execution.
 
     Base class that gathers common operations for any OTB application lifetime (settings, exec, export, etc.)
-    There are several ways to pass parameters to init the app. *args can be :
-        - dictionary containing key-arguments enumeration. Useful when a key is python-reserved
-            (e.g. "in") or contains reserved characters such as a point (e.g."mode.extent.unit")
-        - string, App or Output, useful when the user wants to specify the input "in"
-        - list, useful when the user wants to specify the input list 'il'
+    Any app parameter may be passed either using a dict of parameters or keyword argument.
+    The first argument can be :  
+    - dictionary containing key-arguments enumeration. Useful when a key is python-reserved (e.g. "in")
+    - string, App or Output, useful when the user wants to specify the input "in"
+    - list, useful when the user wants to specify the input list 'il'
 
     Args:
         appname: name of the OTB application to initialize, e.g. 'BandMath'
         *args: used to pass an app input as argument and omitting the key
         frozen: freeze OTB app in order avoid blocking during __init___
-        quiet: whether to print logs of the OTB app
+        quiet: whether to print logs of the OTB app and the default progress bar
         name: custom name that will show up in logs, appname will be used if not provided
         **kwargs: used for passing application parameters (e.g. il=["image_1.tif", "image_1.tif"])
 
-- 
GitLab


From 12881408bcb8308f9834d0df6e9eaebb6ed3e147 Mon Sep 17 00:00:00 2001
From: Vincent Delbar <vincent.delbar@latelescop.fr>
Date: Mon, 6 Nov 2023 15:57:02 +0100
Subject: [PATCH 23/30] DOC: enh class docstring

---
 pyotb/core.py | 106 ++++++++++++++++++++++++--------------------------
 1 file changed, 51 insertions(+), 55 deletions(-)

diff --git a/pyotb/core.py b/pyotb/core.py
index d6a60d6..29535f3 100644
--- a/pyotb/core.py
+++ b/pyotb/core.py
@@ -517,7 +517,7 @@ class App(OTBObject):
 
     Base class that gathers common operations for any OTB application lifetime (settings, exec, export, etc.)
     Any app parameter may be passed either using a dict of parameters or keyword argument.
-    The first argument can be :  
+    The first argument can be :
     - dictionary containing key-arguments enumeration. Useful when a key is python-reserved (e.g. "in")
     - string, App or Output, useful when the user wants to specify the input "in"
     - list, useful when the user wants to specify the input list 'il'
@@ -1103,7 +1103,22 @@ class App(OTBObject):
 
 
 class Slicer(App):
-    """Slicer objects i.e. when we call something like raster[:, :, 2] from Python."""
+    """Slicer objects, automatically created when using slicing e.g. app[:, :, 2].
+
+    It contains :
+    - an ExtractROI app that handles extracting bands and ROI and can be written to disk or used in pipelines
+    - in case the user only wants to extract one band, an expression such as "im1b#"
+
+    Args:
+        obj: input
+        rows: slice along Y / Latitude axis
+        cols: slice along X / Longitude axis
+        channels: bands to extract
+
+    Raises:
+        TypeError: if channels param isn't slice, list or int
+
+    """
 
     def __init__(
         self,
@@ -1112,22 +1127,7 @@ class Slicer(App):
         cols: slice,
         channels: slice | list[int] | int,
     ):
-        """Create a slicer object, that can be used directly for writing or inside a BandMath.
-
-        It contains :
-        - an ExtractROI app that handles extracting bands and ROI and can be written to disk or used in pipelines
-        - in case the user only wants to extract one band, an expression such as "im1b#"
-
-        Args:
-            obj: input
-            rows: slice along Y / Latitude axis
-            cols: slice along X / Longitude axis
-            channels: bands to extract
-
-        Raises:
-            TypeError: if channels param isn't slice, list or int
-
-        """
+        """Create a slicer object, that can be used directly for writing or inside a BandMath."""
         super().__init__(
             "ExtractROI",
             obj,
@@ -1189,6 +1189,16 @@ class Slicer(App):
 class Operation(App):
     """Class for arithmetic/math operations done in Python.
 
+    Given some inputs and an operator, this object enables to python operator to a BandMath operation.
+    Operations generally involve 2 inputs (+, -...). It can have only 1 input for `abs` operator.
+    It can have 3 inputs for the ternary operator `cond ? x : y`.
+
+    Args:
+        operator: (str) one of +, -, *, /, >, <, >=, <=, ==, !=, &, |, abs, ?
+        *inputs: operands of the expression to build
+        nb_bands: optionally specify the output nb of bands - used only internally by pyotb.where
+        name: override the default Operation name
+
     Example:
         Consider the python expression (input1 + 2 * input2)  >  0.
         This class enables to create a BandMathX app, with expression such as (im2 + 2 * im1) > 0 ? 1 : 0
@@ -1208,18 +1218,7 @@ class Operation(App):
     """
 
     def __init__(self, operator: str, *inputs, nb_bands: int = None, name: str = None):
-        """Given some inputs and an operator, this object enables to python operator to a BandMath operation.
-
-        Operations generally involve 2 inputs (+, -...). It can have only 1 input for `abs` operator.
-        It can have 3 inputs for the ternary operator `cond ? x : y`.
-
-        Args:
-            operator: (str) one of +, -, *, /, >, <, >=, <=, ==, !=, &, |, abs, ?
-            *inputs: operands of the expression to build
-            nb_bands: optionally specify the output nb of bands - used only internally by pyotb.where
-            name: override the default Operation name
-
-        """
+        """Operation constructor, one part of the logic is handled by App.__create_operator"""
         self.operator = operator
         # We first create a 'fake' expression. E.g for the operation `input1 + input2`
         # we create a fake expression like "str(input1) + str(input2)"
@@ -1426,21 +1425,18 @@ class LogicalOperation(Operation):
     """A specialization of Operation class for boolean logical operations.
 
     Supported operators are >, <, >=, <=, ==, !=, `&` and `|`.
-
     The only difference is that not only the BandMath expression is saved (e.g. "im1b1 > 0 ? 1 : 0"),
      but also the logical expression (e.g. "im1b1 > 0")
 
+    Args:
+        operator: string operator (one of >, <, >=, <=, ==, !=, &, |)
+        *inputs: inputs
+        nb_bands: optionally specify the output nb of bands - used only by pyotb.where
+
     """
 
     def __init__(self, operator: str, *inputs, nb_bands: int = None):
-        """Constructor for a LogicalOperation object.
-
-        Args:
-            operator: string operator (one of >, <, >=, <=, ==, !=, &, |)
-            *inputs: inputs
-            nb_bands: optionally specify the output nb of bands - used only by pyotb.where
-
-        """
+        """Constructor for a LogicalOperation object."""
         self.logical_fake_exp_bands = []
         super().__init__(operator, *inputs, nb_bands=nb_bands, name="LogicalOperation")
         self.logical_exp_bands, self.logical_exp = self.get_real_exp(
@@ -1487,15 +1483,15 @@ class LogicalOperation(Operation):
 
 
 class Input(App):
-    """Class for transforming a filepath to pyOTB object."""
+    """Class for transforming a filepath to pyOTB object.
 
-    def __init__(self, filepath: str):
-        """Default constructor.
+    Args:
+        filepath: Anything supported by GDAL (local file on the filesystem, remote resource, etc.)
 
-        Args:
-            filepath: Anything supported by GDAL (local file on the filesystem, remote resource, etc.)
+    """
 
-        """
+    def __init__(self, filepath: str):
+        """Initialize an ExtractROI OTB app from a filepath, set dtype and store filepath."""
         super().__init__("ExtractROI", {"in": filepath}, quiet=True, frozen=True)
         self._name = f"Input from {filepath}"
         if not filepath.startswith(("/vsi", "http://", "https://", "ftp://")):
@@ -1510,7 +1506,15 @@ class Input(App):
 
 
 class Output(OTBObject):
-    """Object that behave like a pointer to a specific application in-memory output or file."""
+    """Object that behave like a pointer to a specific application in-memory output or file.
+
+    Args:
+        pyotb_app: The pyotb App to store reference from
+        param_key: Output parameter key of the target app
+        filepath: path of the output file (if not memory)
+        mkdir: create missing parent directories
+
+    """
 
     _filepath: str | Path = None
 
@@ -1522,15 +1526,7 @@ class Output(OTBObject):
         filepath: str = None,
         mkdir: bool = True,
     ):
-        """Constructor for an Output object.
-
-        Args:
-            pyotb_app: The pyotb App to store reference from
-            param_key: Output parameter key of the target app
-            filepath: path of the output file (if not memory)
-            mkdir: create missing parent directories
-
-        """
+        """Constructor for an Output object, initialized during App.__init__."""
         self.parent_pyotb_app = pyotb_app  # keep trace of parent app
         self.param_key = param_key
         self.filepath = filepath
-- 
GitLab


From 212e40feebade49a6cb65a35bd788fb3805050a5 Mon Sep 17 00:00:00 2001
From: Vincent Delbar <vincent.delbar@latelescop.fr>
Date: Mon, 6 Nov 2023 16:04:18 +0100
Subject: [PATCH 24/30] DOC: format indent to fix bulletpoints

---
 pyotb/core.py | 20 +++++++++-----------
 1 file changed, 9 insertions(+), 11 deletions(-)

diff --git a/pyotb/core.py b/pyotb/core.py
index 29535f3..7f60d0b 100644
--- a/pyotb/core.py
+++ b/pyotb/core.py
@@ -467,10 +467,8 @@ class OTBObject(ABC):
         """Override the default __getitem__ behaviour.
 
         This function enables 2 things :
-        - slicing, i.e. selecting ROI/bands. For example, selecting first 3 bands: object[:, :, :3]
-                                                          selecting bands 1, 2 & 5 : object[:, :, [0, 1, 4]]
-                                                          selecting 1000x1000 subset : object[:1000, :1000]
-        - access pixel value(s) at a specified row, col index
+            - slicing, i.e. selecting ROI/bands
+            - access pixel value(s) at a specified row, col index
 
         Args:
             key: attribute key
@@ -517,10 +515,10 @@ class App(OTBObject):
 
     Base class that gathers common operations for any OTB application lifetime (settings, exec, export, etc.)
     Any app parameter may be passed either using a dict of parameters or keyword argument.
-    The first argument can be :
-    - dictionary containing key-arguments enumeration. Useful when a key is python-reserved (e.g. "in")
-    - string, App or Output, useful when the user wants to specify the input "in"
-    - list, useful when the user wants to specify the input list 'il'
+    The first argument can be:
+        - dictionary containing key-arguments enumeration. Useful when a key is python-reserved (e.g. "in")
+        - string, App or Output, useful when the user wants to specify the input "in"
+        - list, useful when the user wants to specify the input list 'il'
 
     Args:
         appname: name of the OTB application to initialize, e.g. 'BandMath'
@@ -1105,9 +1103,9 @@ class App(OTBObject):
 class Slicer(App):
     """Slicer objects, automatically created when using slicing e.g. app[:, :, 2].
 
-    It contains :
-    - an ExtractROI app that handles extracting bands and ROI and can be written to disk or used in pipelines
-    - in case the user only wants to extract one band, an expression such as "im1b#"
+    It contains:
+        - an ExtractROI app that handles extracting bands and ROI and can be written to disk or used in pipelines
+        - in case the user only wants to extract one band, an expression such as "im1b#"
 
     Args:
         obj: input
-- 
GitLab


From 36d0892b9303c6a948e8a3af16465898f0885ce7 Mon Sep 17 00:00:00 2001
From: Vincent Delbar <vincent.delbar@latelescop.fr>
Date: Mon, 6 Nov 2023 16:17:21 +0100
Subject: [PATCH 25/30] DOC: enh docstrings with bulletpoints

---
 pyotb/core.py | 19 +++++++++----------
 1 file changed, 9 insertions(+), 10 deletions(-)

diff --git a/pyotb/core.py b/pyotb/core.py
index 7f60d0b..47109da 100644
--- a/pyotb/core.py
+++ b/pyotb/core.py
@@ -515,6 +515,7 @@ class App(OTBObject):
 
     Base class that gathers common operations for any OTB application lifetime (settings, exec, export, etc.)
     Any app parameter may be passed either using a dict of parameters or keyword argument.
+
     The first argument can be:
         - dictionary containing key-arguments enumeration. Useful when a key is python-reserved (e.g. "in")
         - string, App or Output, useful when the user wants to specify the input "in"
@@ -820,6 +821,7 @@ class App(OTBObject):
             - filepath, useful when there is only one output, e.g. 'output.tif'
             - dictionary containing output filepath
             - None if output file was passed during App init
+
         In case of multiple outputs, pixel_type may also be a dictionary with parameter names as keys.
         Accepted pixel types : uint8, uint16, uint32, int16, int32, float, double, cint16, cint32, cfloat, cdouble
 
@@ -1103,9 +1105,8 @@ class App(OTBObject):
 class Slicer(App):
     """Slicer objects, automatically created when using slicing e.g. app[:, :, 2].
 
-    It contains:
-        - an ExtractROI app that handles extracting bands and ROI and can be written to disk or used in pipelines
-        - in case the user only wants to extract one band, an expression such as "im1b#"
+    Can be used to select a subset of pixel and / or bands in the image.
+    This is a shortcut to an ExtractROI app that can be written to disk or used in pipelines.
 
     Args:
         obj: input
@@ -1423,8 +1424,8 @@ class LogicalOperation(Operation):
     """A specialization of Operation class for boolean logical operations.
 
     Supported operators are >, <, >=, <=, ==, !=, `&` and `|`.
-    The only difference is that not only the BandMath expression is saved (e.g. "im1b1 > 0 ? 1 : 0"),
-     but also the logical expression (e.g. "im1b1 > 0")
+    The only difference is that not only the BandMath expression is saved
+     (e.g. "im1b1 > 0 ? 1 : 0"), but also the logical expression (e.g. "im1b1 > 0")
 
     Args:
         operator: string operator (one of >, <, >=, <=, ==, !=, &, |)
@@ -1449,8 +1450,8 @@ class LogicalOperation(Operation):
     ):
         """Create a list of 'fake' expressions, one for each band.
 
-        e.g for the operation input1 > input2, we create a fake expression that is like
-        "str(input1) > str(input2) ? 1 : 0" and a logical fake expression that is like "str(input1) > str(input2)"
+        For the operation input1 > input2, we create a fake expression like `str(input1) > str(input2) ? 1 : 0`
+         and a logical fake expression like `str(input1) > str(input2)`
 
         Args:
             operator: str (one of >, <, >=, <=, ==, !=, &, |)
@@ -1630,9 +1631,7 @@ def get_nbchannels(inp: str | Path | OTBObject) -> int:
         try:
             info = App("ReadImageInfo", inp, quiet=True)
             return info["numberbands"]
-        except (
-            RuntimeError
-        ) as info_err:  # this happens when we pass a str that is not a filepath
+        except RuntimeError as info_err:  # e.g. file is missing
             raise TypeError(
                 f"Could not get the number of channels file '{inp}' ({info_err})"
             ) from info_err
-- 
GitLab


From 8a131d01d0b642a8971489763973a77821a4b422 Mon Sep 17 00:00:00 2001
From: Vincent Delbar <vincent.delbar@latelescop.fr>
Date: Mon, 6 Nov 2023 16:23:29 +0100
Subject: [PATCH 26/30] DOC: add missing dot

---
 pyotb/core.py | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/pyotb/core.py b/pyotb/core.py
index 47109da..346dc31 100644
--- a/pyotb/core.py
+++ b/pyotb/core.py
@@ -1217,7 +1217,7 @@ class Operation(App):
     """
 
     def __init__(self, operator: str, *inputs, nb_bands: int = None, name: str = None):
-        """Operation constructor, one part of the logic is handled by App.__create_operator"""
+        """Operation constructor, one part of the logic is handled by App.__create_operator."""
         self.operator = operator
         # We first create a 'fake' expression. E.g for the operation `input1 + input2`
         # we create a fake expression like "str(input1) + str(input2)"
-- 
GitLab


From b16538db952eec7e012816f4e7d88993c1860d8a Mon Sep 17 00:00:00 2001
From: Vincent Delbar <vincent.delbar@latelescop.fr>
Date: Mon, 6 Nov 2023 16:37:59 +0100
Subject: [PATCH 27/30] ENH: docstrings

---
 pyotb/core.py | 10 ++++++----
 1 file changed, 6 insertions(+), 4 deletions(-)

diff --git a/pyotb/core.py b/pyotb/core.py
index 346dc31..bc6529e 100644
--- a/pyotb/core.py
+++ b/pyotb/core.py
@@ -517,9 +517,9 @@ class App(OTBObject):
     Any app parameter may be passed either using a dict of parameters or keyword argument.
 
     The first argument can be:
-        - dictionary containing key-arguments enumeration. Useful when a key is python-reserved (e.g. "in")
-        - string, App or Output, useful when the user wants to specify the input "in"
+        - string, App or Output, the main input parameter name is automatically set
         - list, useful when the user wants to specify the input list 'il'
+        - dictionary containing key-arguments enumeration. Useful when a key is python-reserved (e.g. "in", "map")
 
     Args:
         appname: name of the OTB application to initialize, e.g. 'BandMath'
@@ -695,8 +695,10 @@ class App(OTBObject):
         return self._time_end - self._time_start
 
     def set_parameters(self, *args, **kwargs):
-        """Set some parameters of the app.
+        """Set parameters, using the right OTB API function depending on the key and type.
 
+        Parameters with dots may be passed as keyword arguments using "_", e.g. map_epsg_code=4326.
+        Additional checks are done for input and output (in-memory objects, remote filepaths, etc.).
         When useful, e.g. for images list, this function appends the parameters
         instead of overwriting them. Handles any parameters, i.e. in-memory & filepaths
 
@@ -1482,7 +1484,7 @@ class LogicalOperation(Operation):
 
 
 class Input(App):
-    """Class for transforming a filepath to pyOTB object.
+    """Class for transforming a filepath to pyotb object.
 
     Args:
         filepath: Anything supported by GDAL (local file on the filesystem, remote resource, etc.)
-- 
GitLab


From 238dce9911046b714eab8baea38ed24dedaeeaf1 Mon Sep 17 00:00:00 2001
From: Vincent Delbar <vincent.delbar@latelescop.fr>
Date: Mon, 6 Nov 2023 16:49:11 +0100
Subject: [PATCH 28/30] ENH: App docstring

---
 pyotb/core.py | 11 ++++++-----
 1 file changed, 6 insertions(+), 5 deletions(-)

diff --git a/pyotb/core.py b/pyotb/core.py
index bc6529e..8afd168 100644
--- a/pyotb/core.py
+++ b/pyotb/core.py
@@ -517,17 +517,18 @@ class App(OTBObject):
     Any app parameter may be passed either using a dict of parameters or keyword argument.
 
     The first argument can be:
-        - string, App or Output, the main input parameter name is automatically set
-        - list, useful when the user wants to specify the input list 'il'
-        - dictionary containing key-arguments enumeration. Useful when a key is python-reserved (e.g. "in", "map")
+        - filepath or OTBObject, the main input parameter name is automatically used
+        - list of inputs, useful when the user wants to specify the input list `il`
+        - dictionary of parameters, useful when a key is python-reserved (e.g. `in`, `map`)
+    Any key except reserved keywards may also be passed via kwargs, if you replace dots with "_" e.g `map_epsg_code=4326`
 
     Args:
         appname: name of the OTB application to initialize, e.g. 'BandMath'
-        *args: used to pass an app input as argument and omitting the key
+        *args: can be a filepath, OTB object or a dict or parameters, several dicts will be merged in **kwargs
         frozen: freeze OTB app in order avoid blocking during __init___
         quiet: whether to print logs of the OTB app and the default progress bar
         name: custom name that will show up in logs, appname will be used if not provided
-        **kwargs: used for passing application parameters (e.g. il=["image_1.tif", "image_1.tif"])
+        **kwargs: any OTB application parameter key is accepted except "in"
 
     """
 
-- 
GitLab


From e55ec155aa0ca9338a66c02d53b6341a081c2fbb Mon Sep 17 00:00:00 2001
From: Vincent Delbar <vincent.delbar@latelescop.fr>
Date: Mon, 6 Nov 2023 17:00:25 +0100
Subject: [PATCH 29/30] STYLE: use comprehensive list instead of filter +
 whitespace

---
 pyotb/core.py | 10 +++++++---
 1 file changed, 7 insertions(+), 3 deletions(-)

diff --git a/pyotb/core.py b/pyotb/core.py
index 8afd168..2b7af8c 100644
--- a/pyotb/core.py
+++ b/pyotb/core.py
@@ -581,6 +581,7 @@ class App(OTBObject):
         self._time_start, self._time_end = 0.0, 0.0
         self.data, self.outputs = {}, {}
         self.quiet, self.frozen = quiet, frozen
+
         # Param keys and types
         self.parameters_keys = tuple(self.app.GetParametersKeys())
         self._all_param_types = {
@@ -596,15 +597,18 @@ class App(OTBObject):
             for key in self.parameters_keys
             if self.app.GetParameterType(key) == otb.ParameterType_Choice
         }
+
         # Init, execute and write (auto flush only when output param was provided)
         if args or kwargs:
             self.set_parameters(*args, **kwargs)
         # Create Output image objects
-        for key in filter(
-            lambda k: self._out_param_types[k] == otb.ParameterType_OutputImage,
-            self._out_param_types,
+        for key in (
+            key
+            for key, param in self._out_param_types.items()
+            if param == otb.ParameterType_OutputImage
         ):
             self.outputs[key] = Output(self, key, self._settings.get(key))
+
         if not self.frozen:
             self.execute()
             if any(key in self._settings for key in self._out_param_types):
-- 
GitLab


From e18bcfc3394a4b1cca5fbe4c22eaee74cb328cf2 Mon Sep 17 00:00:00 2001
From: Vincent Delbar <vincent.delbar@latelescop.fr>
Date: Mon, 6 Nov 2023 18:37:43 +0100
Subject: [PATCH 30/30] CI: typo

---
 pyotb/core.py | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/pyotb/core.py b/pyotb/core.py
index 2b7af8c..9cd9d4e 100644
--- a/pyotb/core.py
+++ b/pyotb/core.py
@@ -520,7 +520,7 @@ class App(OTBObject):
         - filepath or OTBObject, the main input parameter name is automatically used
         - list of inputs, useful when the user wants to specify the input list `il`
         - dictionary of parameters, useful when a key is python-reserved (e.g. `in`, `map`)
-    Any key except reserved keywards may also be passed via kwargs, if you replace dots with "_" e.g `map_epsg_code=4326`
+    Any key except "in" or "map" can also be passed via kwargs, replace "." with "_" e.g `map_epsg_code=4326`
 
     Args:
         appname: name of the OTB application to initialize, e.g. 'BandMath'
@@ -528,7 +528,7 @@ class App(OTBObject):
         frozen: freeze OTB app in order avoid blocking during __init___
         quiet: whether to print logs of the OTB app and the default progress bar
         name: custom name that will show up in logs, appname will be used if not provided
-        **kwargs: any OTB application parameter key is accepted except "in"
+        **kwargs: any OTB application parameter key is accepted except "in" or "map"
 
     """
 
-- 
GitLab