From 8827e4a250603efc5f9b63ed9c1fa937de866b16 Mon Sep 17 00:00:00 2001 From: Remi Cresson <remi.cresson@inrae.fr> Date: Wed, 28 Jun 2023 09:36:01 +0200 Subject: [PATCH 01/23] WIP: implement extended filenames as dict --- pyotb/core.py | 312 ++++++++++++++++++++++++++++++-------------------- 1 file changed, 188 insertions(+), 124 deletions(-) diff --git a/pyotb/core.py b/pyotb/core.py index 06da0c7..268b284 100644 --- a/pyotb/core.py +++ b/pyotb/core.py @@ -87,7 +87,7 @@ 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 = None ) -> list[int | float] | int | float: """Get pixel value(s) at a given YX coordinates. @@ -101,7 +101,8 @@ class OTBObject(ABC): """ channels = [] - app = App("PixelValue", self, coordx=col, coordy=row, frozen=True, quiet=True) + app = App("PixelValue", self, coordx=col, coordy=row, frozen=True, + quiet=True) if bands is not None: if isinstance(bands, int): if bands < 0: @@ -115,7 +116,8 @@ class OTBObject(ABC): ) if channels: app.app.Execute() - app.set_parameters({"cl": [f"Channel{n + 1}" for n in channels]}) + app.set_parameters( + {"cl": [f"Channel{n + 1}" for n in channels]}) app.execute() data = literal_eval(app.app.GetParameterString("value")) return data[0] if len(channels) == 1 else data @@ -124,8 +126,10 @@ class OTBObject(ABC): """Get list of channels to read values at, from a slice.""" 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 - stop = nb_channels + stop if isinstance(stop, int) and stop < 0 else stop + start = nb_channels + start if isinstance(start, + int) and start < 0 else start + stop = nb_channels + stop if isinstance(stop, + int) and stop < 0 else stop step = 1 if step is None else step if start is not None and stop is not None: return list(range(start, stop, step)) @@ -140,7 +144,7 @@ class OTBObject(ABC): ) def export( - self, key: str = None, preserve_dtype: bool = True + self, key: str = None, preserve_dtype: bool = True ) -> dict[str, dict[str, np.ndarray]]: """Export a specific output image as numpy array and store it in object exports_dic. @@ -158,13 +162,15 @@ class OTBObject(ABC): if key not in self.exports_dic: self.exports_dic[key] = self.app.ExportImage(key) if preserve_dtype: - self.exports_dic[key]["array"] = self.exports_dic[key]["array"].astype( + self.exports_dic[key]["array"] = self.exports_dic[key][ + "array"].astype( self.dtype ) return self.exports_dic[key] def to_numpy( - self, key: str = None, preserve_dtype: bool = True, copy: bool = False + self, key: str = None, preserve_dtype: bool = True, + copy: bool = False ) -> np.ndarray: """Export a pyotb object to numpy array. @@ -193,7 +199,8 @@ class OTBObject(ABC): profile = {} array = self.to_numpy(preserve_dtype=True, copy=False) proj = self.app.GetImageProjection(self.output_image_key) - profile.update({"crs": proj, "dtype": array.dtype, "transform": self.transform}) + profile.update( + {"crs": proj, "dtype": array.dtype, "transform": self.transform}) height, width, count = array.shape profile.update({"count": count, "height": height, "width": width}) return np.moveaxis(array, 2, 0), profile @@ -294,7 +301,8 @@ class OTBObject(ABC): """Logical or.""" return self.__create_operator(LogicalOperation, "||", self, other) - def __and__(self, other: OTBObject | str | int | float) -> LogicalOperation: + def __and__(self, + other: OTBObject | str | int | float) -> LogicalOperation: """Logical and.""" return self.__create_operator(LogicalOperation, "&&", self, other) @@ -390,7 +398,7 @@ class OTBObject(ABC): return self.get_values_at_coords(row, col, channels) # Slicing if not isinstance(key, tuple) or ( - isinstance(key, tuple) and (len(key) < 2 or len(key) > 3) + isinstance(key, tuple) and (len(key) < 2 or len(key) > 3) ): raise ValueError( f'"{key}" cannot be interpreted as valid slicing. Slicing should be 2D or 3D.' @@ -446,13 +454,13 @@ class App(OTBObject): ] def __init__( - self, - appname: str, - *args, - frozen: bool = False, - quiet: bool = False, - name: str = "", - **kwargs, + self, + appname: str, + *args, + frozen: bool = False, + quiet: bool = False, + name: str = "", + **kwargs, ): """Common constructor for OTB applications. Handles in-memory connection between apps. @@ -502,8 +510,9 @@ class App(OTBObject): 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, + lambda k: self._out_param_types[ + k] == otb.ParameterType_OutputImage, + self._out_param_types, ): self.outputs[key] = Output(self, key, self._settings.get(key)) if not self.frozen: @@ -524,7 +533,8 @@ class App(OTBObject): @property def parameters(self): """Return used OTB application parameters.""" - return {**self._auto_parameters, **self.app.GetParameters(), **self._settings} + return {**self._auto_parameters, **self.app.GetParameters(), + **self._settings} @property def exports_dic(self) -> dict[str, dict]: @@ -534,7 +544,8 @@ class App(OTBObject): def __is_one_of_types(self, key: str, param_types: list[int]) -> bool: """Helper to factor is_input and is_output.""" if key not in self._all_param_types: - raise KeyError(f"key {key} not found in the application parameters types") + raise KeyError( + f"key {key} not found in the application parameters types") return self._all_param_types[key] in param_types def is_input(self, key: str) -> bool: @@ -547,7 +558,8 @@ class App(OTBObject): True if the parameter is an input, else False """ - return self.__is_one_of_types(key=key, param_types=self.INPUT_PARAM_TYPES) + 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. @@ -559,7 +571,8 @@ class App(OTBObject): True if the parameter is an output, else False """ - return self.__is_one_of_types(key=key, param_types=self.OUTPUT_PARAM_TYPES) + return self.__is_one_of_types(key=key, + param_types=self.OUTPUT_PARAM_TYPES) def is_key_list(self, key: str) -> bool: """Check if a parameter key is an input parameter list.""" @@ -683,7 +696,8 @@ class App(OTBObject): dtype = get_pixel_type(param) except (TypeError, RuntimeError): logger.warning( - '%s: unable to identify pixel type of key "%s"', self.name, param + '%s: unable to identify pixel type of key "%s"', self.name, + param ) return if target_key: @@ -699,7 +713,8 @@ class App(OTBObject): def execute(self): """Execute and write to disk if any output parameter has been set during init.""" - logger.debug("%s: run execute() with parameters=%s", self.name, self.parameters) + logger.debug("%s: run execute() with parameters=%s", self.name, + self.parameters) self._time_start = perf_counter() try: self.app.Execute() @@ -727,28 +742,36 @@ class App(OTBObject): self._time_end = perf_counter() def write( - self, - path: str | Path | dict[str, str] = None, - pixel_type: dict[str, str] | str = None, - preserve_dtype: bool = False, - ext_fname: str = "", - **kwargs, + self, + path: str | Path | dict[str, str] = None, + pixel_type: dict[str, str] | str = None, + preserve_dtype: bool = False, + ext_fname: str | dict[str, str] | dict[str, int] | dict[ + str, float] = None, + **kwargs, ) -> bool: """Set output pixel type and write the output raster files. 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 - ext_fname: Optional, an extended filename as understood by OTB (e.g. "&gdal:co:TILED=YES") - Will be used for all outputs (Default value = "") - 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) - preserve_dtype: propagate main input pixel type to outputs, in case pixel_type is None + 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 + ext_fname: Optional, an extended filename as understood by OTB + (e.g. "&gdal:co:TILED=YES") or a dict of key/value. Will be + used for all outputs. + 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. + preserve_dtype: propagate main input pixel type to outputs, in + case pixel_type is None **kwargs: keyword arguments e.g. out='output.tif' Returns: @@ -767,28 +790,47 @@ class App(OTBObject): elif isinstance(path, (str, Path)) and self.output_key: kwargs.update({self.output_key: str(path)}) elif path is not None: - raise TypeError(f"{self.name}: unsupported filepath type ({type(path)})") - if not (kwargs or any(k in self._settings for k in self._out_param_types)): + raise TypeError( + f"{self.name}: unsupported filepath type ({type(path)})") + if not (kwargs or any( + k in self._settings for k in self._out_param_types)): raise KeyError( f"{self.name}: at least one filepath is required, if not provided during App init" ) parameters = kwargs.copy() # Append filename extension to filenames + assert isinstance(ext_fname, (dict, str)) if ext_fname: - logger.debug( - "%s: using extended filename for outputs: %s", self.name, ext_fname - ) - if not ext_fname.startswith("?"): - ext_fname = "?&" + ext_fname - elif not ext_fname.startswith("?&"): - ext_fname = "?&" + ext_fname[1:] - for key, value in kwargs.items(): - if ( - self._out_param_types[key] == otb.ParameterType_OutputImage - and "?" not in value - ): - parameters[key] = value + ext_fname + + def _str2dict(ext_str): + """Function that converts str to dict.""" + return dict( + kv for pair in ext_str.split("&") + if len(kv := pair.split("=")) == 2 + ) + + if isinstance(ext_fname, str): + # transform str to dict + ext_fname = _str2dict(ext_fname) + + logger.debug("%s: extended filename for outputs: %s", self.name) + for key, ext in ext_fname.items(): + logger.debug("%s: %s", key, ext) + + for key, filepath in kwargs.items(): + if self._out_param_types[key] == otb.ParameterType_OutputImage: + # grab already set extended filename key/values + already_set_ext = _str2dict(filepath.split("?&")[0]) \ + if "?&" in filepath else {} + + # transform dict to str + ext_fname_str = "&".join([ + f"{k}={v}" for k, v in ext_fname.items() + if k not in already_set_ext + ]) + parameters[key] = f"{filepath}?{ext_fname_str}" + # Manage output pixel types data_types = {} if pixel_type: @@ -796,14 +838,17 @@ class App(OTBObject): dtype = parse_pixel_type(pixel_type) type_name = self.app.ConvertPixelTypeToNumpy(dtype) logger.debug( - '%s: output(s) will be written with type "%s"', self.name, type_name + '%s: output(s) will be written with type "%s"', self.name, + type_name ) for key in parameters: - if self._out_param_types[key] == otb.ParameterType_OutputImage: + if self._out_param_types[ + key] == otb.ParameterType_OutputImage: data_types[key] = dtype elif isinstance(pixel_type, dict): data_types = { - key: parse_pixel_type(dtype) for key, dtype in pixel_type.items() + 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 @@ -836,7 +881,8 @@ class App(OTBObject): return bool(files) and not missing # Private functions - def __parse_args(self, args: list[str | OTBObject | dict | list]) -> dict[str, Any]: + def __parse_args(self, args: list[str | OTBObject | dict | list]) -> dict[ + str, Any]: """Gather all input arguments in kwargs dict. Args: @@ -851,15 +897,16 @@ class App(OTBObject): if isinstance(arg, dict): kwargs.update(arg) elif ( - isinstance(arg, (str, OTBObject)) - or isinstance(arg, list) - and self.is_key_list(self.input_key) + isinstance(arg, (str, OTBObject)) + or isinstance(arg, list) + and self.is_key_list(self.input_key) ): kwargs.update({self.input_key: arg}) return kwargs def __set_param( - self, key: str, obj: list | tuple | OTBObject | otb.Application | list[Any] + self, key: str, + obj: list | tuple | OTBObject | otb.Application | list[Any] ): """Set one parameter, decide which otb.Application method to use depending on target object.""" if obj is None or (isinstance(obj, (list, tuple)) and not obj): @@ -869,11 +916,11 @@ class App(OTBObject): if isinstance(obj, OTBObject): self.app.ConnectImage(key, obj.app, obj.output_image_key) elif isinstance( - obj, otb.Application + obj, otb.Application ): # this is for backward comp with plain OTB self.app.ConnectImage(key, obj, get_out_images_param_keys(obj)[0]) elif ( - key == "ram" + key == "ram" ): # SetParameterValue in OTB<7.4 doesn't work for ram parameter cf gitlab OTB issue 2200 self.app.SetParameterInt("ram", int(obj)) elif not isinstance(obj, list): # any other parameters (str, int...) @@ -885,9 +932,10 @@ class App(OTBObject): if isinstance(inp, OTBObject): self.app.ConnectImage(key, inp.app, inp.output_image_key) elif isinstance( - inp, otb.Application + inp, otb.Application ): # this is for backward comp with plain OTB - self.app.ConnectImage(key, obj, get_out_images_param_keys(inp)[0]) + self.app.ConnectImage(key, obj, + get_out_images_param_keys(inp)[0]) 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) @@ -902,8 +950,9 @@ class App(OTBObject): continue value = self.app.GetParameterValue(key) # TODO: here we *should* use self.app.IsParameterEnabled, but it seems broken - if isinstance(value, otb.ApplicationProxy) and self.app.HasAutomaticValue( - key + if isinstance(value, + otb.ApplicationProxy) and self.app.HasAutomaticValue( + key ): try: value = str( @@ -914,7 +963,8 @@ class App(OTBObject): except RuntimeError: continue # grouped parameters # Save static output data (ReadImageInfo, ComputeImageStatistics, etc.) - elif self.app.GetParameterRole(key) == 1 and bool(value) or value == 0: + elif self.app.GetParameterRole(key) == 1 and bool( + value) or value == 0: if isinstance(value, str): try: value = literal_eval(value) @@ -923,7 +973,8 @@ 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) -> 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 @@ -937,7 +988,8 @@ class App(OTBObject): return self.outputs[key] if key in self.parameters: return self.parameters[key] - raise KeyError(f"{self.name}: unknown or undefined parameter '{key}'") + raise KeyError( + f"{self.name}: unknown or undefined parameter '{key}'") raise TypeError( f"{self.name}: cannot access object item or slice using {type(key)} object" ) @@ -947,11 +999,11 @@ class Slicer(App): """Slicer objects i.e. when we call something like raster[:, :, 2] from Python.""" def __init__( - self, - obj: OTBObject, - rows: slice, - cols: slice, - channels: slice | list[int] | int, + self, + obj: OTBObject, + rows: slice, + cols: slice, + channels: slice | list[int] | int, ): """Create a slicer object, that can be used directly for writing or inside a BandMath. @@ -1018,9 +1070,10 @@ class Slicer(App): ) # subtract 1 to respect python convention spatial_slicing = True # These are some attributes 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: + if not spatial_slicing and isinstance(channels, list) and len( + channels) == 1: self.one_band_sliced = ( - channels[0] + 1 + channels[0] + 1 ) # OTB convention: channels start at 1 self.input = obj @@ -1051,7 +1104,8 @@ class Operation(App): """ - def __init__(self, operator: str, *inputs, nb_bands: int = None, name: str = None): + 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. Operations generally involve 2 inputs (+, -...). It can have only 1 input for `abs` operator. @@ -1098,10 +1152,10 @@ class Operation(App): ) def build_fake_expressions( - self, - operator: str, - inputs: list[OTBObject | str | int | float], - nb_bands: int = None, + self, + operator: str, + inputs: list[OTBObject | str | int | float], + nb_bands: int = None, ): """Create a list of 'fake' expressions, one for each band. @@ -1122,8 +1176,8 @@ class Operation(App): # 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 + isinstance(inp, Slicer) and hasattr(inp, "one_band_sliced") + for inp in inputs ): nb_bands = 1 else: @@ -1134,9 +1188,10 @@ class Operation(App): ] # 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 + x == nb_bands_list[0] for x in nb_bands_list ): - raise ValueError("All images do not have the same number of bands") + raise ValueError( + "All images do not have the same number of bands") nb_bands = nb_bands_list[0] # Create a list of fake expressions, each item of the list corresponding to one band @@ -1171,7 +1226,7 @@ class Operation(App): # 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 == "?" + 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) @@ -1193,14 +1248,15 @@ class Operation(App): one_band_exp = one_band_fake_exp for inp in self.inputs: # Replace the name of in-memory object (e.g. '<pyotb.App object>b1' by 'im1b1') - one_band_exp = one_band_exp.replace(repr(inp), self.im_dic[repr(inp)]) + one_band_exp = one_band_exp.replace(repr(inp), + self.im_dic[repr(inp)]) exp_bands.append(one_band_exp) # Form the final expression (e.g. 'im1b1 + 1; im1b2 + 1') return exp_bands, ";".join(exp_bands) @staticmethod def make_fake_exp( - x: OTBObject | str, band: int, keep_logical: bool = False + x: OTBObject | str, band: int, keep_logical: bool = False ) -> tuple[str, list[OTBObject], int]: """This an internal function, only to be used by `build_fake_expressions`. @@ -1223,7 +1279,8 @@ class Operation(App): # Special case for one-band slicer if isinstance(x, Slicer) and hasattr(x, "one_band_sliced"): if keep_logical and isinstance(x.input, LogicalOperation): - fake_exp = x.input.logical_fake_exp_bands[x.one_band_sliced - 1] + fake_exp = x.input.logical_fake_exp_bands[ + x.one_band_sliced - 1] inputs, nb_channels = x.input.inputs, x.input.nb_channels elif isinstance(x.input, Operation): # Keep only one band of the expression @@ -1275,16 +1332,17 @@ class LogicalOperation(Operation): """ self.logical_fake_exp_bands = [] - super().__init__(operator, *inputs, nb_bands=nb_bands, name="LogicalOperation") + super().__init__(operator, *inputs, nb_bands=nb_bands, + name="LogicalOperation") self.logical_exp_bands, self.logical_exp = self.get_real_exp( self.logical_fake_exp_bands ) def build_fake_expressions( - self, - operator: str, - inputs: list[OTBObject | str | int | float], - nb_bands: int = None, + self, + operator: str, + inputs: list[OTBObject | str | int | float], + nb_bands: int = None, ): """Create a list of 'fake' expressions, one for each band. @@ -1299,8 +1357,8 @@ class LogicalOperation(Operation): """ # 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 + isinstance(inp, Slicer) and hasattr(inp, "one_band_sliced") + for inp in inputs ): nb_bands = 1 else: @@ -1311,9 +1369,10 @@ class LogicalOperation(Operation): ] # 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 + x == nb_bands_list[0] for x in nb_bands_list ): - raise ValueError("All images do not have the same number of bands") + 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)): @@ -1366,11 +1425,11 @@ class Output(OTBObject): _filepath: str | Path = None def __init__( - self, - pyotb_app: App, - param_key: str = None, - filepath: str = None, - mkdir: bool = True, + self, + pyotb_app: App, + param_key: str = None, + filepath: str = None, + mkdir: bool = True, ): """Constructor for an Output object. @@ -1417,7 +1476,8 @@ class Output(OTBObject): @filepath.setter def filepath(self, path: str): if isinstance(path, str): - if path and not path.startswith(("/vsi", "http://", "https://", "ftp://")): + if path and not path.startswith( + ("/vsi", "http://", "https://", "ftp://")): path = Path(path.split("?")[0]) self._filepath = path @@ -1439,7 +1499,8 @@ class Output(OTBObject): return self.parent_pyotb_app.write( {self.output_image_key: self.filepath}, **kwargs ) - return self.parent_pyotb_app.write({self.output_image_key: filepath}, **kwargs) + return self.parent_pyotb_app.write({self.output_image_key: filepath}, + **kwargs) def __str__(self) -> str: """Return string representation of Output filepath.""" @@ -1496,12 +1557,13 @@ def get_nbchannels(inp: str | Path | OTBObject) -> int: info = App("ReadImageInfo", inp, quiet=True) return info["numberbands"] except ( - RuntimeError + RuntimeError ) as info_err: # this happens when we pass a str that is not a filepath raise TypeError( f"Could not get the number of channels file '{inp}' ({info_err})" ) from info_err - raise TypeError(f"Can't read number of channels of type '{type(inp)}' object {inp}") + raise TypeError( + f"Can't read number of channels of type '{type(inp)}' object {inp}") def get_pixel_type(inp: str | Path | OTBObject) -> str: @@ -1522,14 +1584,15 @@ def get_pixel_type(inp: str | Path | OTBObject) -> str: info = App("ReadImageInfo", inp, quiet=True) datatype = info["datatype"] # which is such as short, float... except ( - RuntimeError + RuntimeError ) as info_err: # this happens when we pass a str that is not a filepath raise TypeError( f"Could not get the pixel type of `{inp}` ({info_err})" ) from info_err if datatype: return parse_pixel_type(datatype) - raise TypeError(f"Could not get the pixel type of {type(inp)} object {inp}") + raise TypeError( + f"Could not get the pixel type of {type(inp)} object {inp}") def parse_pixel_type(pixel_type: str | int) -> int: @@ -1559,7 +1622,8 @@ def parse_pixel_type(pixel_type: str | int) -> int: if pixel_type in datatype_to_pixeltype.values(): return getattr(otb, f"ImagePixelType_{pixel_type}") if pixel_type in datatype_to_pixeltype: - return getattr(otb, f"ImagePixelType_{datatype_to_pixeltype[pixel_type]}") + return getattr(otb, + f"ImagePixelType_{datatype_to_pixeltype[pixel_type]}") raise KeyError( f"Unknown data type `{pixel_type}`. Available ones: {datatype_to_pixeltype}" ) @@ -1578,9 +1642,9 @@ def get_out_images_param_keys(app: OTBObject) -> list[str]: def summarize( - obj: App | Output | Any, - strip_input_paths: bool = False, - strip_output_paths: bool = False, + obj: App | Output | Any, + strip_input_paths: bool = False, + strip_output_paths: bool = False, ) -> dict[str, str | dict[str, Any]]: """Recursively summarize parameters of an App or Output object and its parents. @@ -1614,10 +1678,10 @@ def summarize( parameters = {} for key, param in obj.parameters.items(): if ( - strip_input_paths - and obj.is_input(key) - or strip_output_paths - and obj.is_output(key) + strip_input_paths + and obj.is_input(key) + or strip_output_paths + and obj.is_output(key) ): parameters[key] = ( [strip_path(p) for p in param] -- GitLab From 437eb5d003a946b1980de2f45756c294599fa3e0 Mon Sep 17 00:00:00 2001 From: Remi Cresson <remi.cresson@inrae.fr> Date: Wed, 28 Jun 2023 09:46:26 +0200 Subject: [PATCH 02/23] WIP: implement extended filenames as dict --- pyotb/core.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyotb/core.py b/pyotb/core.py index 268b284..c50ca4d 100644 --- a/pyotb/core.py +++ b/pyotb/core.py @@ -800,9 +800,9 @@ class App(OTBObject): parameters = kwargs.copy() # Append filename extension to filenames - assert isinstance(ext_fname, (dict, str)) if ext_fname: - + assert isinstance(ext_fname, (dict, str)) + def _str2dict(ext_str): """Function that converts str to dict.""" return dict( -- GitLab From 9beba5f4f0b5d1b72da13baf7b758aa1a406b357 Mon Sep 17 00:00:00 2001 From: Remi Cresson <remi.cresson@inrae.fr> Date: Wed, 28 Jun 2023 09:52:51 +0200 Subject: [PATCH 03/23] WIP: implement extended filenames as dict --- pyotb/core.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyotb/core.py b/pyotb/core.py index c50ca4d..af667aa 100644 --- a/pyotb/core.py +++ b/pyotb/core.py @@ -802,7 +802,7 @@ class App(OTBObject): # Append filename extension to filenames if ext_fname: assert isinstance(ext_fname, (dict, str)) - + def _str2dict(ext_str): """Function that converts str to dict.""" return dict( @@ -829,7 +829,7 @@ class App(OTBObject): f"{k}={v}" for k, v in ext_fname.items() if k not in already_set_ext ]) - parameters[key] = f"{filepath}?{ext_fname_str}" + parameters[key] = f"{filepath}?&{ext_fname_str}" # Manage output pixel types data_types = {} -- GitLab From 4ac0b869bc22a54489736a0ea5521aeeb0720fe7 Mon Sep 17 00:00:00 2001 From: Remi Cresson <remi.cresson@inrae.fr> Date: Wed, 28 Jun 2023 09:56:15 +0200 Subject: [PATCH 04/23] ADD: test for extended filename --- tests/test_core.py | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/tests/test_core.py b/tests/test_core.py index 99b21de..b19e92b 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -1,6 +1,5 @@ import pytest -import pyotb from tests_data import * @@ -111,6 +110,24 @@ def test_write(): INPUT["out"].filepath.unlink() +def test_ext_fname(): + assert INPUT.write("/tmp/test_write.tif", ext_fname="nodata=0") + assert INPUT.write("/tmp/test_write.tif", ext_fname={"nodata": "0"}) + assert INPUT.write("/tmp/test_write.tif", ext_fname={"nodata": 0}) + assert INPUT.write( + "/tmp/test_write.tif", + ext_fname={ + "nodata": 0, + "gdal:co:COMPRESS": "DEFLATE" + } + ) + assert INPUT.write( + "/tmp/test_write.tif", + ext_fname="nodata=0&gdal:co:COMPRESS=DEFLATE" + ) + INPUT["out"].filepath.unlink() + + def test_frozen_app_write(): app = pyotb.BandMath(INPUT, exp="im1b1", frozen=True) assert app.write("/tmp/test_frozen_app_write.tif") -- GitLab From c37e717fa6b4ad0400b06ad7499e4893c5f3b3e7 Mon Sep 17 00:00:00 2001 From: Remi Cresson <remi.cresson@inrae.fr> Date: Wed, 28 Jun 2023 09:56:31 +0200 Subject: [PATCH 05/23] DOC: pydocstyle --- pyotb/core.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/pyotb/core.py b/pyotb/core.py index af667aa..0cc74a5 100644 --- a/pyotb/core.py +++ b/pyotb/core.py @@ -802,19 +802,18 @@ class App(OTBObject): # Append filename extension to filenames if ext_fname: assert isinstance(ext_fname, (dict, str)) - def _str2dict(ext_str): """Function that converts str to dict.""" return dict( - kv for pair in ext_str.split("&") - if len(kv := pair.split("=")) == 2 + keyval for pair in ext_str.split("&") + if len(keyval := pair.split("=")) == 2 ) if isinstance(ext_fname, str): # transform str to dict ext_fname = _str2dict(ext_fname) - logger.debug("%s: extended filename for outputs: %s", self.name) + logger.debug("%s: extended filename for outputs:", self.name) for key, ext in ext_fname.items(): logger.debug("%s: %s", key, ext) -- GitLab From 3c4459a5c0d4a8150e5209b2dddba9bc17c4cc24 Mon Sep 17 00:00:00 2001 From: Remi Cresson <remi.cresson@inrae.fr> Date: Wed, 28 Jun 2023 10:07:41 +0200 Subject: [PATCH 06/23] STY: back to original indentation --- pyotb/core.py | 269 +++++++++++++++++++++----------------------------- 1 file changed, 111 insertions(+), 158 deletions(-) diff --git a/pyotb/core.py b/pyotb/core.py index 0cc74a5..b776a3d 100644 --- a/pyotb/core.py +++ b/pyotb/core.py @@ -87,7 +87,7 @@ 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 = None ) -> list[int | float] | int | float: """Get pixel value(s) at a given YX coordinates. @@ -101,8 +101,7 @@ class OTBObject(ABC): """ channels = [] - app = App("PixelValue", self, coordx=col, coordy=row, frozen=True, - quiet=True) + app = App("PixelValue", self, coordx=col, coordy=row, frozen=True, quiet=True) if bands is not None: if isinstance(bands, int): if bands < 0: @@ -116,8 +115,7 @@ class OTBObject(ABC): ) if channels: app.app.Execute() - app.set_parameters( - {"cl": [f"Channel{n + 1}" for n in channels]}) + app.set_parameters({"cl": [f"Channel{n + 1}" for n in channels]}) app.execute() data = literal_eval(app.app.GetParameterString("value")) return data[0] if len(channels) == 1 else data @@ -126,10 +124,8 @@ class OTBObject(ABC): """Get list of channels to read values at, from a slice.""" 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 - stop = nb_channels + stop if isinstance(stop, - int) and stop < 0 else stop + start = nb_channels + start if isinstance(start, int) and start < 0 else start + stop = nb_channels + stop if isinstance(stop, int) and stop < 0 else stop step = 1 if step is None else step if start is not None and stop is not None: return list(range(start, stop, step)) @@ -144,7 +140,7 @@ class OTBObject(ABC): ) def export( - self, key: str = None, preserve_dtype: bool = True + self, key: str = None, preserve_dtype: bool = True ) -> dict[str, dict[str, np.ndarray]]: """Export a specific output image as numpy array and store it in object exports_dic. @@ -162,15 +158,13 @@ class OTBObject(ABC): if key not in self.exports_dic: self.exports_dic[key] = self.app.ExportImage(key) if preserve_dtype: - self.exports_dic[key]["array"] = self.exports_dic[key][ - "array"].astype( + self.exports_dic[key]["array"] = self.exports_dic[key]["array"].astype( self.dtype ) return self.exports_dic[key] def to_numpy( - self, key: str = None, preserve_dtype: bool = True, - copy: bool = False + self, key: str = None, preserve_dtype: bool = True, copy: bool = False ) -> np.ndarray: """Export a pyotb object to numpy array. @@ -199,8 +193,7 @@ class OTBObject(ABC): profile = {} array = self.to_numpy(preserve_dtype=True, copy=False) proj = self.app.GetImageProjection(self.output_image_key) - profile.update( - {"crs": proj, "dtype": array.dtype, "transform": self.transform}) + profile.update({"crs": proj, "dtype": array.dtype, "transform": self.transform}) height, width, count = array.shape profile.update({"count": count, "height": height, "width": width}) return np.moveaxis(array, 2, 0), profile @@ -301,8 +294,7 @@ class OTBObject(ABC): """Logical or.""" return self.__create_operator(LogicalOperation, "||", self, other) - def __and__(self, - other: OTBObject | str | int | float) -> LogicalOperation: + def __and__(self, other: OTBObject | str | int | float) -> LogicalOperation: """Logical and.""" return self.__create_operator(LogicalOperation, "&&", self, other) @@ -398,7 +390,7 @@ class OTBObject(ABC): return self.get_values_at_coords(row, col, channels) # Slicing if not isinstance(key, tuple) or ( - isinstance(key, tuple) and (len(key) < 2 or len(key) > 3) + isinstance(key, tuple) and (len(key) < 2 or len(key) > 3) ): raise ValueError( f'"{key}" cannot be interpreted as valid slicing. Slicing should be 2D or 3D.' @@ -454,13 +446,13 @@ class App(OTBObject): ] def __init__( - self, - appname: str, - *args, - frozen: bool = False, - quiet: bool = False, - name: str = "", - **kwargs, + self, + appname: str, + *args, + frozen: bool = False, + quiet: bool = False, + name: str = "", + **kwargs, ): """Common constructor for OTB applications. Handles in-memory connection between apps. @@ -510,9 +502,8 @@ class App(OTBObject): 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, + lambda k: self._out_param_types[k] == otb.ParameterType_OutputImage, + self._out_param_types, ): self.outputs[key] = Output(self, key, self._settings.get(key)) if not self.frozen: @@ -533,8 +524,7 @@ class App(OTBObject): @property def parameters(self): """Return used OTB application parameters.""" - return {**self._auto_parameters, **self.app.GetParameters(), - **self._settings} + return {**self._auto_parameters, **self.app.GetParameters(), **self._settings} @property def exports_dic(self) -> dict[str, dict]: @@ -544,8 +534,7 @@ class App(OTBObject): def __is_one_of_types(self, key: str, param_types: list[int]) -> bool: """Helper to factor is_input and is_output.""" if key not in self._all_param_types: - raise KeyError( - f"key {key} not found in the application parameters types") + raise KeyError(f"key {key} not found in the application parameters types") return self._all_param_types[key] in param_types def is_input(self, key: str) -> bool: @@ -558,8 +547,7 @@ class App(OTBObject): True if the parameter is an input, else False """ - return self.__is_one_of_types(key=key, - param_types=self.INPUT_PARAM_TYPES) + 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. @@ -571,8 +559,7 @@ class App(OTBObject): True if the parameter is an output, else False """ - return self.__is_one_of_types(key=key, - param_types=self.OUTPUT_PARAM_TYPES) + return self.__is_one_of_types(key=key, param_types=self.OUTPUT_PARAM_TYPES) def is_key_list(self, key: str) -> bool: """Check if a parameter key is an input parameter list.""" @@ -696,8 +683,7 @@ class App(OTBObject): dtype = get_pixel_type(param) except (TypeError, RuntimeError): logger.warning( - '%s: unable to identify pixel type of key "%s"', self.name, - param + '%s: unable to identify pixel type of key "%s"', self.name, param ) return if target_key: @@ -713,8 +699,7 @@ class App(OTBObject): def execute(self): """Execute and write to disk if any output parameter has been set during init.""" - logger.debug("%s: run execute() with parameters=%s", self.name, - self.parameters) + logger.debug("%s: run execute() with parameters=%s", self.name, self.parameters) self._time_start = perf_counter() try: self.app.Execute() @@ -742,36 +727,28 @@ class App(OTBObject): self._time_end = perf_counter() def write( - self, - path: str | Path | dict[str, str] = None, - pixel_type: dict[str, str] | str = None, - preserve_dtype: bool = False, - ext_fname: str | dict[str, str] | dict[str, int] | dict[ - str, float] = None, - **kwargs, + self, + path: str | Path | dict[str, str] = None, + pixel_type: dict[str, str] | str = None, + preserve_dtype: bool = False, + ext_fname: str = "", + **kwargs, ) -> bool: """Set output pixel type and write the output raster files. 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 - ext_fname: Optional, an extended filename as understood by OTB - (e.g. "&gdal:co:TILED=YES") or a dict of key/value. Will be - used for all outputs. - 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. - preserve_dtype: propagate main input pixel type to outputs, in - case pixel_type is None + 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 + ext_fname: Optional, an extended filename as understood by OTB (e.g. "&gdal:co:TILED=YES") + Will be used for all outputs (Default value = "") + 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) + preserve_dtype: propagate main input pixel type to outputs, in case pixel_type is None **kwargs: keyword arguments e.g. out='output.tif' Returns: @@ -790,10 +767,8 @@ class App(OTBObject): elif isinstance(path, (str, Path)) and self.output_key: kwargs.update({self.output_key: str(path)}) elif path is not None: - raise TypeError( - f"{self.name}: unsupported filepath type ({type(path)})") - if not (kwargs or any( - k in self._settings for k in self._out_param_types)): + raise TypeError(f"{self.name}: unsupported filepath type ({type(path)})") + if not (kwargs or any(k in self._settings for k in self._out_param_types)): raise KeyError( f"{self.name}: at least one filepath is required, if not provided during App init" ) @@ -837,17 +812,14 @@ class App(OTBObject): dtype = parse_pixel_type(pixel_type) type_name = self.app.ConvertPixelTypeToNumpy(dtype) logger.debug( - '%s: output(s) will be written with type "%s"', self.name, - type_name + '%s: output(s) will be written with type "%s"', self.name, type_name ) for key in parameters: - if self._out_param_types[ - key] == otb.ParameterType_OutputImage: + if self._out_param_types[key] == otb.ParameterType_OutputImage: data_types[key] = dtype elif isinstance(pixel_type, dict): data_types = { - key: parse_pixel_type(dtype) for key, dtype in - pixel_type.items() + 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 @@ -880,8 +852,7 @@ class App(OTBObject): return bool(files) and not missing # Private functions - def __parse_args(self, args: list[str | OTBObject | dict | list]) -> dict[ - str, Any]: + def __parse_args(self, args: list[str | OTBObject | dict | list]) -> dict[str, Any]: """Gather all input arguments in kwargs dict. Args: @@ -896,16 +867,15 @@ class App(OTBObject): if isinstance(arg, dict): kwargs.update(arg) elif ( - isinstance(arg, (str, OTBObject)) - or isinstance(arg, list) - and self.is_key_list(self.input_key) + isinstance(arg, (str, OTBObject)) + or isinstance(arg, list) + and self.is_key_list(self.input_key) ): kwargs.update({self.input_key: arg}) return kwargs def __set_param( - self, key: str, - obj: list | tuple | OTBObject | otb.Application | list[Any] + self, key: str, obj: list | tuple | OTBObject | otb.Application | list[Any] ): """Set one parameter, decide which otb.Application method to use depending on target object.""" if obj is None or (isinstance(obj, (list, tuple)) and not obj): @@ -915,11 +885,11 @@ class App(OTBObject): if isinstance(obj, OTBObject): self.app.ConnectImage(key, obj.app, obj.output_image_key) elif isinstance( - obj, otb.Application + obj, otb.Application ): # this is for backward comp with plain OTB self.app.ConnectImage(key, obj, get_out_images_param_keys(obj)[0]) elif ( - key == "ram" + key == "ram" ): # SetParameterValue in OTB<7.4 doesn't work for ram parameter cf gitlab OTB issue 2200 self.app.SetParameterInt("ram", int(obj)) elif not isinstance(obj, list): # any other parameters (str, int...) @@ -931,10 +901,9 @@ class App(OTBObject): if isinstance(inp, OTBObject): self.app.ConnectImage(key, inp.app, inp.output_image_key) elif isinstance( - inp, otb.Application + inp, otb.Application ): # this is for backward comp with plain OTB - self.app.ConnectImage(key, obj, - get_out_images_param_keys(inp)[0]) + self.app.ConnectImage(key, obj, get_out_images_param_keys(inp)[0]) 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) @@ -949,9 +918,8 @@ class App(OTBObject): continue value = self.app.GetParameterValue(key) # TODO: here we *should* use self.app.IsParameterEnabled, but it seems broken - if isinstance(value, - otb.ApplicationProxy) and self.app.HasAutomaticValue( - key + if isinstance(value, otb.ApplicationProxy) and self.app.HasAutomaticValue( + key ): try: value = str( @@ -962,8 +930,7 @@ class App(OTBObject): except RuntimeError: continue # grouped parameters # Save static output data (ReadImageInfo, ComputeImageStatistics, etc.) - elif self.app.GetParameterRole(key) == 1 and bool( - value) or value == 0: + elif self.app.GetParameterRole(key) == 1 and bool(value) or value == 0: if isinstance(value, str): try: value = literal_eval(value) @@ -972,8 +939,7 @@ 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) -> 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 @@ -987,8 +953,7 @@ class App(OTBObject): return self.outputs[key] if key in self.parameters: return self.parameters[key] - raise KeyError( - f"{self.name}: unknown or undefined parameter '{key}'") + raise KeyError(f"{self.name}: unknown or undefined parameter '{key}'") raise TypeError( f"{self.name}: cannot access object item or slice using {type(key)} object" ) @@ -998,11 +963,11 @@ class Slicer(App): """Slicer objects i.e. when we call something like raster[:, :, 2] from Python.""" def __init__( - self, - obj: OTBObject, - rows: slice, - cols: slice, - channels: slice | list[int] | int, + self, + obj: OTBObject, + rows: slice, + cols: slice, + channels: slice | list[int] | int, ): """Create a slicer object, that can be used directly for writing or inside a BandMath. @@ -1069,10 +1034,9 @@ class Slicer(App): ) # subtract 1 to respect python convention spatial_slicing = True # These are some attributes 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: + if not spatial_slicing and isinstance(channels, list) and len(channels) == 1: self.one_band_sliced = ( - channels[0] + 1 + channels[0] + 1 ) # OTB convention: channels start at 1 self.input = obj @@ -1103,8 +1067,7 @@ class Operation(App): """ - def __init__(self, operator: str, *inputs, nb_bands: int = None, - name: str = None): + 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. Operations generally involve 2 inputs (+, -...). It can have only 1 input for `abs` operator. @@ -1151,10 +1114,10 @@ class Operation(App): ) def build_fake_expressions( - self, - operator: str, - inputs: list[OTBObject | str | int | float], - nb_bands: int = None, + self, + operator: str, + inputs: list[OTBObject | str | int | float], + nb_bands: int = None, ): """Create a list of 'fake' expressions, one for each band. @@ -1175,8 +1138,8 @@ class Operation(App): # 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 + isinstance(inp, Slicer) and hasattr(inp, "one_band_sliced") + for inp in inputs ): nb_bands = 1 else: @@ -1187,10 +1150,9 @@ class Operation(App): ] # 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 + x == nb_bands_list[0] for x in nb_bands_list ): - raise ValueError( - "All images do not have the same number of bands") + raise ValueError("All images do not have the same number of bands") nb_bands = nb_bands_list[0] # Create a list of fake expressions, each item of the list corresponding to one band @@ -1225,7 +1187,7 @@ class Operation(App): # 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 == "?" + 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) @@ -1247,15 +1209,14 @@ class Operation(App): one_band_exp = one_band_fake_exp for inp in self.inputs: # Replace the name of in-memory object (e.g. '<pyotb.App object>b1' by 'im1b1') - one_band_exp = one_band_exp.replace(repr(inp), - self.im_dic[repr(inp)]) + one_band_exp = one_band_exp.replace(repr(inp), self.im_dic[repr(inp)]) exp_bands.append(one_band_exp) # Form the final expression (e.g. 'im1b1 + 1; im1b2 + 1') return exp_bands, ";".join(exp_bands) @staticmethod def make_fake_exp( - x: OTBObject | str, band: int, keep_logical: bool = False + x: OTBObject | str, band: int, keep_logical: bool = False ) -> tuple[str, list[OTBObject], int]: """This an internal function, only to be used by `build_fake_expressions`. @@ -1278,8 +1239,7 @@ class Operation(App): # Special case for one-band slicer if isinstance(x, Slicer) and hasattr(x, "one_band_sliced"): if keep_logical and isinstance(x.input, LogicalOperation): - fake_exp = x.input.logical_fake_exp_bands[ - x.one_band_sliced - 1] + fake_exp = x.input.logical_fake_exp_bands[x.one_band_sliced - 1] inputs, nb_channels = x.input.inputs, x.input.nb_channels elif isinstance(x.input, Operation): # Keep only one band of the expression @@ -1331,17 +1291,16 @@ class LogicalOperation(Operation): """ self.logical_fake_exp_bands = [] - super().__init__(operator, *inputs, nb_bands=nb_bands, - name="LogicalOperation") + super().__init__(operator, *inputs, nb_bands=nb_bands, name="LogicalOperation") self.logical_exp_bands, self.logical_exp = self.get_real_exp( self.logical_fake_exp_bands ) def build_fake_expressions( - self, - operator: str, - inputs: list[OTBObject | str | int | float], - nb_bands: int = None, + self, + operator: str, + inputs: list[OTBObject | str | int | float], + nb_bands: int = None, ): """Create a list of 'fake' expressions, one for each band. @@ -1356,8 +1315,8 @@ class LogicalOperation(Operation): """ # 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 + isinstance(inp, Slicer) and hasattr(inp, "one_band_sliced") + for inp in inputs ): nb_bands = 1 else: @@ -1368,10 +1327,9 @@ class LogicalOperation(Operation): ] # 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 + x == nb_bands_list[0] for x in nb_bands_list ): - raise ValueError( - "All images do not have the same number of bands") + 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)): @@ -1424,11 +1382,11 @@ class Output(OTBObject): _filepath: str | Path = None def __init__( - self, - pyotb_app: App, - param_key: str = None, - filepath: str = None, - mkdir: bool = True, + self, + pyotb_app: App, + param_key: str = None, + filepath: str = None, + mkdir: bool = True, ): """Constructor for an Output object. @@ -1475,8 +1433,7 @@ class Output(OTBObject): @filepath.setter def filepath(self, path: str): if isinstance(path, str): - if path and not path.startswith( - ("/vsi", "http://", "https://", "ftp://")): + if path and not path.startswith(("/vsi", "http://", "https://", "ftp://")): path = Path(path.split("?")[0]) self._filepath = path @@ -1498,8 +1455,7 @@ class Output(OTBObject): return self.parent_pyotb_app.write( {self.output_image_key: self.filepath}, **kwargs ) - return self.parent_pyotb_app.write({self.output_image_key: filepath}, - **kwargs) + return self.parent_pyotb_app.write({self.output_image_key: filepath}, **kwargs) def __str__(self) -> str: """Return string representation of Output filepath.""" @@ -1556,13 +1512,12 @@ def get_nbchannels(inp: str | Path | OTBObject) -> int: info = App("ReadImageInfo", inp, quiet=True) return info["numberbands"] except ( - RuntimeError + RuntimeError ) as info_err: # this happens when we pass a str that is not a filepath raise TypeError( f"Could not get the number of channels file '{inp}' ({info_err})" ) from info_err - raise TypeError( - f"Can't read number of channels of type '{type(inp)}' object {inp}") + raise TypeError(f"Can't read number of channels of type '{type(inp)}' object {inp}") def get_pixel_type(inp: str | Path | OTBObject) -> str: @@ -1583,15 +1538,14 @@ def get_pixel_type(inp: str | Path | OTBObject) -> str: info = App("ReadImageInfo", inp, quiet=True) datatype = info["datatype"] # which is such as short, float... except ( - RuntimeError + RuntimeError ) as info_err: # this happens when we pass a str that is not a filepath raise TypeError( f"Could not get the pixel type of `{inp}` ({info_err})" ) from info_err if datatype: return parse_pixel_type(datatype) - raise TypeError( - f"Could not get the pixel type of {type(inp)} object {inp}") + raise TypeError(f"Could not get the pixel type of {type(inp)} object {inp}") def parse_pixel_type(pixel_type: str | int) -> int: @@ -1621,8 +1575,7 @@ def parse_pixel_type(pixel_type: str | int) -> int: if pixel_type in datatype_to_pixeltype.values(): return getattr(otb, f"ImagePixelType_{pixel_type}") if pixel_type in datatype_to_pixeltype: - return getattr(otb, - f"ImagePixelType_{datatype_to_pixeltype[pixel_type]}") + return getattr(otb, f"ImagePixelType_{datatype_to_pixeltype[pixel_type]}") raise KeyError( f"Unknown data type `{pixel_type}`. Available ones: {datatype_to_pixeltype}" ) @@ -1641,9 +1594,9 @@ def get_out_images_param_keys(app: OTBObject) -> list[str]: def summarize( - obj: App | Output | Any, - strip_input_paths: bool = False, - strip_output_paths: bool = False, + obj: App | Output | Any, + strip_input_paths: bool = False, + strip_output_paths: bool = False, ) -> dict[str, str | dict[str, Any]]: """Recursively summarize parameters of an App or Output object and its parents. @@ -1677,10 +1630,10 @@ def summarize( parameters = {} for key, param in obj.parameters.items(): if ( - strip_input_paths - and obj.is_input(key) - or strip_output_paths - and obj.is_output(key) + strip_input_paths + and obj.is_input(key) + or strip_output_paths + and obj.is_output(key) ): parameters[key] = ( [strip_path(p) for p in param] -- GitLab From aa2ae0245b482d3683a9df9080b8d1d804be31af Mon Sep 17 00:00:00 2001 From: Remi Cresson <remi.cresson@inrae.fr> Date: Wed, 28 Jun 2023 10:36:44 +0200 Subject: [PATCH 07/23] FIX: ext_fname prevail over extensions in filepath --- pyotb/core.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/pyotb/core.py b/pyotb/core.py index b776a3d..52b3ffc 100644 --- a/pyotb/core.py +++ b/pyotb/core.py @@ -795,13 +795,14 @@ class App(OTBObject): for key, filepath in kwargs.items(): if self._out_param_types[key] == otb.ParameterType_OutputImage: # grab already set extended filename key/values - already_set_ext = _str2dict(filepath.split("?&")[0]) \ - if "?&" in filepath else {} + if "?&" in filepath: + filepath, already_set_ext = filepath.split("?&", 1) + # ext_fname prevail over extensions in filepath + ext_fname = {**_str2dict(already_set_ext), **ext_fname} # transform dict to str ext_fname_str = "&".join([ - f"{k}={v}" for k, v in ext_fname.items() - if k not in already_set_ext + f"{key}={value}" for key, value in ext_fname.items() ]) parameters[key] = f"{filepath}?&{ext_fname_str}" -- GitLab From 2b526505f832959a0c75c6b0d1cc3dd74fb1446c Mon Sep 17 00:00:00 2001 From: Remi Cresson <remi.cresson@inrae.fr> Date: Wed, 28 Jun 2023 10:50:08 +0200 Subject: [PATCH 08/23] TEST: strenghten ext_fname tests --- tests/test_core.py | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/tests/test_core.py b/tests/test_core.py index b19e92b..8d2c1e2 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -111,9 +111,17 @@ def test_write(): def test_ext_fname(): + def _check(expected: str): + fn = INPUT.app.GetParameterString("out") + assert "?&" in fn + assert fn.split("?&", 1) == expected + assert INPUT.write("/tmp/test_write.tif", ext_fname="nodata=0") + _check("nodata=0") assert INPUT.write("/tmp/test_write.tif", ext_fname={"nodata": "0"}) + _check("nodata=0") assert INPUT.write("/tmp/test_write.tif", ext_fname={"nodata": 0}) + _check("nodata=0") assert INPUT.write( "/tmp/test_write.tif", ext_fname={ @@ -121,10 +129,28 @@ def test_ext_fname(): "gdal:co:COMPRESS": "DEFLATE" } ) + _check("nodata=0&gdal:co:COMPRESS=DEFLATE") assert INPUT.write( "/tmp/test_write.tif", ext_fname="nodata=0&gdal:co:COMPRESS=DEFLATE" ) + _check("nodata=0&gdal:co:COMPRESS=DEFLATE") + assert INPUT.write( + "/tmp/test_write.tif?&box=0:0:10:10", + ext_fname={ + "nodata": "0", + "gdal:co:COMPRESS": "DEFLATE", + "box": "0:0:20:20" + } + ) + _check("box=0:0:20:20&nodata=0&gdal:co:COMPRESS=DEFLATE") + _check("nodata=0&gdal:co:COMPRESS=DEFLATE") + assert INPUT.write( + "/tmp/test_write.tif?&box=0:0:10:10", + ext_fname="nodata=0&gdal:co:COMPRESS=DEFLATE&box=0:0:20:20" + ) + _check("box=0:0:20:20&nodata=0&gdal:co:COMPRESS=DEFLATE") + INPUT["out"].filepath.unlink() -- GitLab From 6af96f3d38197a72077c026ed6b815bac8e2f49a Mon Sep 17 00:00:00 2001 From: Remi Cresson <remi.cresson@inrae.fr> Date: Wed, 28 Jun 2023 11:00:24 +0200 Subject: [PATCH 09/23] TEST: strenghten ext_fname tests --- tests/test_core.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_core.py b/tests/test_core.py index 8d2c1e2..ea18808 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -114,7 +114,7 @@ def test_ext_fname(): def _check(expected: str): fn = INPUT.app.GetParameterString("out") assert "?&" in fn - assert fn.split("?&", 1) == expected + assert fn.split("?&", 1)[1] == expected assert INPUT.write("/tmp/test_write.tif", ext_fname="nodata=0") _check("nodata=0") -- GitLab From 38f320dcbbd5d1dbb80e638cd65a811607b3e502 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9mi=20Cresson?= <remi.cresson@inrae.fr> Date: Wed, 28 Jun 2023 09:06:56 +0000 Subject: [PATCH 10/23] Apply 1 suggestion(s) to 1 file(s) --- pyotb/core.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pyotb/core.py b/pyotb/core.py index 52b3ffc..bddd15c 100644 --- a/pyotb/core.py +++ b/pyotb/core.py @@ -776,7 +776,8 @@ class App(OTBObject): # Append filename extension to filenames if ext_fname: - assert isinstance(ext_fname, (dict, str)) + if not isinstance(ext_fname, (dict, str)): + raise ValueError("Extended filename must be a str or a dict") def _str2dict(ext_str): """Function that converts str to dict.""" return dict( -- GitLab From 0fc010c6c3a93dcdadb61f065faded705282b007 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9mi=20Cresson?= <remi.cresson@inrae.fr> Date: Wed, 28 Jun 2023 09:11:42 +0000 Subject: [PATCH 11/23] Apply 1 suggestion(s) to 1 file(s) --- pyotb/core.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyotb/core.py b/pyotb/core.py index bddd15c..94faa6b 100644 --- a/pyotb/core.py +++ b/pyotb/core.py @@ -799,7 +799,7 @@ class App(OTBObject): if "?&" in filepath: filepath, already_set_ext = filepath.split("?&", 1) # ext_fname prevail over extensions in filepath - ext_fname = {**_str2dict(already_set_ext), **ext_fname} + ext_fname.update(_str2dict(already_set_ext)) # transform dict to str ext_fname_str = "&".join([ -- GitLab From d1cdccfd05853ec8ab3603a179b0624ddccc69f6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9mi=20Cresson?= <remi.cresson@inrae.fr> Date: Wed, 28 Jun 2023 09:12:30 +0000 Subject: [PATCH 12/23] Apply 2 suggestion(s) to 1 file(s) --- tests/test_core.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_core.py b/tests/test_core.py index ea18808..1a628c2 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -143,13 +143,13 @@ def test_ext_fname(): "box": "0:0:20:20" } ) - _check("box=0:0:20:20&nodata=0&gdal:co:COMPRESS=DEFLATE") + _check("box=0:0:10:10&nodata=0&gdal:co:COMPRESS=DEFLATE") _check("nodata=0&gdal:co:COMPRESS=DEFLATE") assert INPUT.write( "/tmp/test_write.tif?&box=0:0:10:10", ext_fname="nodata=0&gdal:co:COMPRESS=DEFLATE&box=0:0:20:20" ) - _check("box=0:0:20:20&nodata=0&gdal:co:COMPRESS=DEFLATE") + _check("box=0:0:10:10&nodata=0&gdal:co:COMPRESS=DEFLATE") INPUT["out"].filepath.unlink() -- GitLab From b80fd7e06c58f1bae1a11b4d11dacb6f5a18a683 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9mi=20Cresson?= <remi.cresson@inrae.fr> Date: Wed, 28 Jun 2023 09:12:48 +0000 Subject: [PATCH 13/23] Apply 1 suggestion(s) to 1 file(s) --- pyotb/core.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/pyotb/core.py b/pyotb/core.py index 94faa6b..e9319a1 100644 --- a/pyotb/core.py +++ b/pyotb/core.py @@ -780,10 +780,8 @@ class App(OTBObject): raise ValueError("Extended filename must be a str or a dict") def _str2dict(ext_str): """Function that converts str to dict.""" - return dict( - keyval for pair in ext_str.split("&") - if len(keyval := pair.split("=")) == 2 - ) + splits = [pair.split("=") for pair in ext_str.split("&")] + return dict(split for split in splits if len(split) == 2) if isinstance(ext_fname, str): # transform str to dict -- GitLab From 5bf540388b3f9fe42e16dd8e9462b13006b390a6 Mon Sep 17 00:00:00 2001 From: Remi Cresson <remi.cresson@inrae.fr> Date: Wed, 28 Jun 2023 14:16:24 +0200 Subject: [PATCH 14/23] ADD: fix ext_fname tests --- tests/test_core.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/tests/test_core.py b/tests/test_core.py index 1a628c2..824195a 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -143,13 +143,14 @@ def test_ext_fname(): "box": "0:0:20:20" } ) - _check("box=0:0:10:10&nodata=0&gdal:co:COMPRESS=DEFLATE") - _check("nodata=0&gdal:co:COMPRESS=DEFLATE") + # Check that the bbox is the one specified in the filepath, not the one + # specified in `ext_filename` + _check("nodata=0&gdal:co:COMPRESS=DEFLATE&box=0:0:10:10") assert INPUT.write( "/tmp/test_write.tif?&box=0:0:10:10", ext_fname="nodata=0&gdal:co:COMPRESS=DEFLATE&box=0:0:20:20" ) - _check("box=0:0:10:10&nodata=0&gdal:co:COMPRESS=DEFLATE") + _check("nodata=0&gdal:co:COMPRESS=DEFLATE&box=0:0:10:10") INPUT["out"].filepath.unlink() -- GitLab From 5b702ca36a5f4c8fad964269ec4b674025e40ad1 Mon Sep 17 00:00:00 2001 From: Remi Cresson <remi.cresson@inrae.fr> Date: Wed, 28 Jun 2023 14:42:54 +0200 Subject: [PATCH 15/23] FIX: copy ext_fname when it's a dict --- pyotb/core.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/pyotb/core.py b/pyotb/core.py index e9319a1..b717acc 100644 --- a/pyotb/core.py +++ b/pyotb/core.py @@ -783,12 +783,11 @@ class App(OTBObject): splits = [pair.split("=") for pair in ext_str.split("&")] return dict(split for split in splits if len(split) == 2) - if isinstance(ext_fname, str): - # transform str to dict - ext_fname = _str2dict(ext_fname) + new_ext_fname = _str2dict(ext_fname) \ + if isinstance(ext_fname, str) else ext_fname.copy() logger.debug("%s: extended filename for outputs:", self.name) - for key, ext in ext_fname.items(): + for key, ext in new_ext_fname.items(): logger.debug("%s: %s", key, ext) for key, filepath in kwargs.items(): @@ -797,11 +796,12 @@ class App(OTBObject): if "?&" in filepath: filepath, already_set_ext = filepath.split("?&", 1) # ext_fname prevail over extensions in filepath - ext_fname.update(_str2dict(already_set_ext)) + new_ext_fname.update(_str2dict(already_set_ext)) # transform dict to str ext_fname_str = "&".join([ - f"{key}={value}" for key, value in ext_fname.items() + f"{key}={value}" + for key, value in new_ext_fname.items() ]) parameters[key] = f"{filepath}?&{ext_fname_str}" -- GitLab From b0122b170ef162b7dfccb0e04d63853582ebc973 Mon Sep 17 00:00:00 2001 From: Remi Cresson <remi.cresson@inrae.fr> Date: Wed, 28 Jun 2023 14:55:23 +0200 Subject: [PATCH 16/23] FIX: copy ext_fname when it's a dict --- pyotb/core.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/pyotb/core.py b/pyotb/core.py index b717acc..7e13e91 100644 --- a/pyotb/core.py +++ b/pyotb/core.py @@ -783,11 +783,11 @@ class App(OTBObject): splits = [pair.split("=") for pair in ext_str.split("&")] return dict(split for split in splits if len(split) == 2) - new_ext_fname = _str2dict(ext_fname) \ + gen_ext_fname = _str2dict(ext_fname) \ if isinstance(ext_fname, str) else ext_fname.copy() - logger.debug("%s: extended filename for outputs:", self.name) - for key, ext in new_ext_fname.items(): + logger.debug("%s: extended filename for all outputs:", self.name) + for key, ext in gen_ext_fname.items(): logger.debug("%s: %s", key, ext) for key, filepath in kwargs.items(): @@ -796,6 +796,7 @@ class App(OTBObject): if "?&" in filepath: filepath, already_set_ext = filepath.split("?&", 1) # ext_fname prevail over extensions in filepath + new_ext_fname = gen_ext_fname.copy() new_ext_fname.update(_str2dict(already_set_ext)) # transform dict to str -- GitLab From d953d9578efca316987930430d1580d870c8c055 Mon Sep 17 00:00:00 2001 From: Remi Cresson <remi.cresson@inrae.fr> Date: Wed, 28 Jun 2023 14:57:50 +0200 Subject: [PATCH 17/23] FIX: copy ext_fname when it's a dict --- pyotb/core.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyotb/core.py b/pyotb/core.py index 7e13e91..a0f4a6f 100644 --- a/pyotb/core.py +++ b/pyotb/core.py @@ -784,7 +784,7 @@ class App(OTBObject): return dict(split for split in splits if len(split) == 2) gen_ext_fname = _str2dict(ext_fname) \ - if isinstance(ext_fname, str) else ext_fname.copy() + if isinstance(ext_fname, str) else ext_fname logger.debug("%s: extended filename for all outputs:", self.name) for key, ext in gen_ext_fname.items(): -- GitLab From e6f14025dca52271b3e8956e583152dd83a8f69e Mon Sep 17 00:00:00 2001 From: Remi Cresson <remi.cresson@inrae.fr> Date: Wed, 28 Jun 2023 14:58:28 +0200 Subject: [PATCH 18/23] FIX: copy ext_fname when it's a dict --- pyotb/core.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyotb/core.py b/pyotb/core.py index a0f4a6f..206a10f 100644 --- a/pyotb/core.py +++ b/pyotb/core.py @@ -795,7 +795,7 @@ class App(OTBObject): # grab already set extended filename key/values if "?&" in filepath: filepath, already_set_ext = filepath.split("?&", 1) - # ext_fname prevail over extensions in filepath + # extensions in filepath prevail over `new_ext_fname` new_ext_fname = gen_ext_fname.copy() new_ext_fname.update(_str2dict(already_set_ext)) -- GitLab From af0b763c21f4e508e0b882d8bc6da6a8ba63f9b4 Mon Sep 17 00:00:00 2001 From: Remi Cresson <remi.cresson@inrae.fr> Date: Wed, 28 Jun 2023 15:00:15 +0200 Subject: [PATCH 19/23] FIX: copy ext_fname when it's a dict --- pyotb/core.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pyotb/core.py b/pyotb/core.py index 206a10f..9d8ee47 100644 --- a/pyotb/core.py +++ b/pyotb/core.py @@ -792,11 +792,12 @@ class App(OTBObject): for key, filepath in kwargs.items(): if self._out_param_types[key] == otb.ParameterType_OutputImage: + new_ext_fname = gen_ext_fname.copy() + # 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` - new_ext_fname = gen_ext_fname.copy() new_ext_fname.update(_str2dict(already_set_ext)) # transform dict to str -- GitLab From 0c2bc2191b1df0e38eb432ad7f518eff4a8ca223 Mon Sep 17 00:00:00 2001 From: Remi Cresson <remi.cresson@inrae.fr> Date: Wed, 28 Jun 2023 15:07:53 +0200 Subject: [PATCH 20/23] ENH: ext_fn tests with 2 outputs --- tests/test_core.py | 23 +++++++++++++++++++++-- 1 file changed, 21 insertions(+), 2 deletions(-) diff --git a/tests/test_core.py b/tests/test_core.py index 824195a..5955e11 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -111,8 +111,8 @@ def test_write(): def test_ext_fname(): - def _check(expected: str): - fn = INPUT.app.GetParameterString("out") + def _check(expected: str, key: str = "out", app = INPUT.app): + fn = app.GetParameterString(key) assert "?&" in fn assert fn.split("?&", 1)[1] == expected @@ -154,6 +154,25 @@ def test_ext_fname(): INPUT["out"].filepath.unlink() + mss = pyotb.MeanShiftSmoothing({ + "in": FILEPATH + }) + mss.write( + { + "fout": "/tmp/test_ext_fn_fout.tif&?nodata=1", + "foutpos": "/tmp/test_ext_fn_foutpos.tif?&nodata=2" + }, + ext_fname={ + "nodata": 0, + "gdal:co:COMPRESS": "DEFLATE" + } + ) + _check("nodata=1&gdal:co:COMPRESS=DEFLATE", key="fout", app=mss.app) + _check("nodata=2&gdal:co:COMPRESS=DEFLATE", key="foutpos", app=mss.app) + mss["fout"].filepath.unlink() + mss["foutpos"].filepath.unlink() + + def test_frozen_app_write(): app = pyotb.BandMath(INPUT, exp="im1b1", frozen=True) -- GitLab From 1e68f1e0b03b1f1fb5d9592982af8b9f1aeae6ef Mon Sep 17 00:00:00 2001 From: Remi Cresson <remi.cresson@inrae.fr> Date: Wed, 28 Jun 2023 15:15:36 +0200 Subject: [PATCH 21/23] ENH: ext_fn tests with 2 outputs --- tests/test_core.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_core.py b/tests/test_core.py index 5955e11..7ca58dd 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -159,7 +159,7 @@ def test_ext_fname(): }) mss.write( { - "fout": "/tmp/test_ext_fn_fout.tif&?nodata=1", + "fout": "/tmp/test_ext_fn_fout.tif?&nodata=1", "foutpos": "/tmp/test_ext_fn_foutpos.tif?&nodata=2" }, ext_fname={ -- GitLab From 68e7fdb77febe206af7bfe5d6a6211e98fe95c09 Mon Sep 17 00:00:00 2001 From: Vincent Delbar <vincent.delbar@latelescop.fr> Date: Wed, 28 Jun 2023 13:26:25 +0000 Subject: [PATCH 22/23] Apply 1 suggestion(s) to 1 file(s) --- pyotb/core.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/pyotb/core.py b/pyotb/core.py index 9d8ee47..58f3f8f 100644 --- a/pyotb/core.py +++ b/pyotb/core.py @@ -783,16 +783,16 @@ class App(OTBObject): splits = [pair.split("=") for pair in ext_str.split("&")] return dict(split for split in splits if len(split) == 2) - gen_ext_fname = _str2dict(ext_fname) \ - if isinstance(ext_fname, str) else ext_fname + if isinstance(ext_fname, str): + ext_fname = _str2dict(ext_fname) logger.debug("%s: extended filename for all outputs:", self.name) - for key, ext in gen_ext_fname.items(): + for key, ext in ext_fname.items(): logger.debug("%s: %s", key, ext) for key, filepath in kwargs.items(): if self._out_param_types[key] == otb.ParameterType_OutputImage: - new_ext_fname = gen_ext_fname.copy() + new_ext_fname = ext_fname.copy() # grab already set extended filename key/values if "?&" in filepath: -- GitLab From 5f837beff8b51d0d7601ce3f6a57f109537f0716 Mon Sep 17 00:00:00 2001 From: Vincent Delbar <vincent.delbar@latelescop.fr> Date: Wed, 28 Jun 2023 13:28:14 +0000 Subject: [PATCH 23/23] Apply 1 suggestion(s) to 1 file(s) --- tests/test_core.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/tests/test_core.py b/tests/test_core.py index 7ca58dd..c3cedef 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -154,9 +154,7 @@ def test_ext_fname(): INPUT["out"].filepath.unlink() - mss = pyotb.MeanShiftSmoothing({ - "in": FILEPATH - }) + mss = pyotb.MeanShiftSmoothing(FILEPATH) mss.write( { "fout": "/tmp/test_ext_fn_fout.tif?&nodata=1", -- GitLab