From a5004a14eb6bc2fd1fb21999f744c4df9b0e6483 Mon Sep 17 00:00:00 2001
From: Vincent Delbar <vincent.delbar@latelescop.fr>
Date: Tue, 4 Jun 2024 12:14:08 +0200
Subject: [PATCH 01/10] Update README.md

---
 README.md | 6 +++---
 1 file changed, 3 insertions(+), 3 deletions(-)

diff --git a/README.md b/README.md
index f5aa184..aaebd5e 100644
--- a/README.md
+++ b/README.md
@@ -1,8 +1,8 @@
 # pyotb: Orfeo ToolBox for Python
 
-[![latest release](https://gitlab.orfeo-toolbox.org/nicolasnn/pyotb/-/badges/release.svg)](https://gitlab.orfeo-toolbox.org/nicolasnn/pyotb/-/releases)
-[![pipeline status](https://gitlab.orfeo-toolbox.org/nicolasnn/pyotb/badges/develop/pipeline.svg)](https://gitlab.orfeo-toolbox.org/nicolasnn/pyotb/-/commits/develop)
-[![coverage report](https://gitlab.orfeo-toolbox.org/nicolasnn/pyotb/badges/develop/coverage.svg)](https://gitlab.orfeo-toolbox.org/nicolasnn/pyotb/-/commits/develop)
+[![latest release](https://forgemia.inra.fr/orfeo-toolbox/pyotb/-/badges/release.svg)](https://gitlab.orfeo-toolbox.org/nicolasnn/pyotb/-/releases)
+[![pipeline status](https://forgemia.inra.fr/orfeo-toolbox/pyotb/badges/develop/pipeline.svg)](https://gitlab.orfeo-toolbox.org/nicolasnn/pyotb/-/commits/develop)
+[![coverage report](https://forgemia.inra.fr/orfeo-toolbox/pyotb/badges/develop/coverage.svg)](https://gitlab.orfeo-toolbox.org/nicolasnn/pyotb/-/commits/develop)
 [![read the docs status](https://readthedocs.org/projects/pyotb/badge/?version=master)](https://pyotb.readthedocs.io/en/master/)
 
 **pyotb** wraps the [Orfeo Toolbox](https://www.orfeo-toolbox.org/) in a pythonic, developer friendly 
-- 
GitLab


From e43063aa1dc1713e2762963cb41e4e0e611efc5f Mon Sep 17 00:00:00 2001
From: Vincent Delbar <vincent.delbar@gmail.com>
Date: Tue, 4 Jun 2024 12:20:57 +0200
Subject: [PATCH 02/10] DOC: update links to forgemia repo

---
 README.md           | 6 +++---
 doc/index.md        | 6 +++---
 doc/installation.md | 2 +-
 mkdocs.yml          | 4 ++--
 4 files changed, 9 insertions(+), 9 deletions(-)

diff --git a/README.md b/README.md
index aaebd5e..93f61f5 100644
--- a/README.md
+++ b/README.md
@@ -1,8 +1,8 @@
 # pyotb: Orfeo ToolBox for Python
 
-[![latest release](https://forgemia.inra.fr/orfeo-toolbox/pyotb/-/badges/release.svg)](https://gitlab.orfeo-toolbox.org/nicolasnn/pyotb/-/releases)
-[![pipeline status](https://forgemia.inra.fr/orfeo-toolbox/pyotb/badges/develop/pipeline.svg)](https://gitlab.orfeo-toolbox.org/nicolasnn/pyotb/-/commits/develop)
-[![coverage report](https://forgemia.inra.fr/orfeo-toolbox/pyotb/badges/develop/coverage.svg)](https://gitlab.orfeo-toolbox.org/nicolasnn/pyotb/-/commits/develop)
+[![latest release](https://forgemia.inra.fr/orfeo-toolbox/pyotb/-/badges/release.svg)](https://forgemia.inra.fr/orfeo-toolbox/pyotb/-/releases)
+[![pipeline status](https://forgemia.inra.fr/orfeo-toolbox/pyotb/badges/develop/pipeline.svg)](https://forgemia.inra.fr/orfeo-toolbox/pyotb/-/commits/develop)
+[![coverage report](https://forgemia.inra.fr/orfeo-toolbox/pyotb/badges/develop/coverage.svg)](https://forgemia.inra.fr/orfeo-toolbox/pyotb/-/commits/develop)
 [![read the docs status](https://readthedocs.org/projects/pyotb/badge/?version=master)](https://pyotb.readthedocs.io/en/master/)
 
 **pyotb** wraps the [Orfeo Toolbox](https://www.orfeo-toolbox.org/) in a pythonic, developer friendly 
diff --git a/doc/index.md b/doc/index.md
index 4ecfaf4..52586a4 100644
--- a/doc/index.md
+++ b/doc/index.md
@@ -37,7 +37,7 @@ on github or gitlab!
 Contributions are welcome !
 Open a PR/MR, or file an issue if you spot a bug or have any suggestion:
 
-- [Github](https://github.com/orfeotoolbox/pyotb) 
-- [Orfeo ToolBox GitLab instance](https://gitlab.orfeo-toolbox.org/nicolasnn/pyotb).
+- [Github](https://github.com/orfeotoolbox/pyotb)
+- [Orfeo ToolBox GitLab instance](https://forgemia.inra.fr/orfeo-toolbox/pyotb).
 
-Thank you!
\ No newline at end of file
+Thank you!
diff --git a/doc/installation.md b/doc/installation.md
index 404d7bd..0414989 100644
--- a/doc/installation.md
+++ b/doc/installation.md
@@ -17,7 +17,7 @@ pip install pyotb --upgrade
 For development, use the following:
 
 ```bash
-git clone https://gitlab.orfeo-toolbox.org/nicolasnn/pyotb
+git clone https://forgemia.inra.fr/orfeo-toolbox/pyotb
 cd pyotb
 pip install -e ".[dev]"
 ```
diff --git a/mkdocs.yml b/mkdocs.yml
index 88589b1..02da75c 100644
--- a/mkdocs.yml
+++ b/mkdocs.yml
@@ -53,7 +53,7 @@ extra:
     tabs: true
   social:
     - icon: fontawesome/brands/gitlab
-      link: https://gitlab.orfeo-toolbox.org/nicolasnn/pyotb
+      link: https://forgemia.inra.fr/orfeo-toolbox/pyotb
 extra_css:
   - https://gitlab.orfeo-toolbox.org/orfeotoolbox/otb/-/raw/8.1.2-rc1/Documentation/Cookbook/_static/css/otb_theme.css
   - extra.css
@@ -76,6 +76,6 @@ markdown_extensions:
 
 # Rest of the navigation.
 site_name: "pyotb: Orfeo ToolBox for Python"
-repo_url: https://gitlab.orfeo-toolbox.org/nicolasnn/pyotb
+repo_url: https://forgemia.inra.fr/orfeo-toolbox/pyotb
 repo_name: pyotb
 docs_dir: doc/
-- 
GitLab


From cfbe54febfecf038d83a65584d33f8080a3519d3 Mon Sep 17 00:00:00 2001
From: Vincent Delbar <vincent.delbar@gmail.com>
Date: Mon, 11 Mar 2024 15:38:52 +0100
Subject: [PATCH 03/10] ENH: better logging config using dict

---
 pyotb/helpers.py | 43 +++++++++++++++++++++++--------------------
 1 file changed, 23 insertions(+), 20 deletions(-)

diff --git a/pyotb/helpers.py b/pyotb/helpers.py
index 0e6ea2a..751016a 100644
--- a/pyotb/helpers.py
+++ b/pyotb/helpers.py
@@ -13,33 +13,36 @@ OTB_ROOT = os.environ.get("OTB_ROOT")
 DOCS_URL = "https://www.orfeo-toolbox.org/CookBook/Installation.html"
 
 # Logging
-# User can also get logger with `logging.getLogger("pyOTB")`
+# User can also get logger with `logging.getLogger("pyotb")`
 # then use pyotb.set_logger_level() to adjust logger verbosity
-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"
-)
-logger_handler.setFormatter(formatter)
+
 # 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"
 )
-logger.setLevel(getattr(logging, LOG_LEVEL))
-# Here it would be possible to use a different level for a specific handler
-# A more verbose one can go to text file while print only errors to stdout
-logger_handler.setLevel(getattr(logging, LOG_LEVEL))
-logger.addHandler(logger_handler)
-
 
-def set_logger_level(level: str):
-    """Allow user to change the current logging level.
+logger = logging.getLogger("pyotb")
 
-    Args:
-        level: logging level string ('DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL')
-    """
-    logger.setLevel(getattr(logging, level))
-    logger_handler.setLevel(getattr(logging, level))
+logging_cfg = {
+    "version": 1,
+    "disable_existing_loggers": False,
+    "formatters": {
+        "default": {
+            "format": "%(asctime)s (%(levelname)-4s) [pyotb] %(message)s",
+            "datefmt": "%Y-%m-%d %H:%M:%S",
+        },
+    },
+    "handlers": {
+        "stdout": {
+            "class": "logging.StreamHandler",
+            "level": LOG_LEVEL,
+            "formatter": "default",
+            "stream": "ext://sys.stdout",
+        }
+    },
+    "loggers": {"pyotb": {"level": LOG_LEVEL, "handlers": ["stdout"]}},
+}
+logging.config.dictConfig(logging_cfg)
 
 
 def find_otb(prefix: str = OTB_ROOT, scan: bool = True):
-- 
GitLab


From 9bb289d077f32cb33d84e70ac5cbc7923ea66882 Mon Sep 17 00:00:00 2001
From: Vincent Delbar <vincent.delbar@gmail.com>
Date: Tue, 4 Jun 2024 16:03:43 +0200
Subject: [PATCH 04/10] ENH: better logger config (preserve rootLogger state)

---
 doc/managing_loggers.md | 14 +++++++-------
 pyotb/__init__.py       |  4 ++--
 pyotb/helpers.py        |  2 ++
 3 files changed, 11 insertions(+), 9 deletions(-)

diff --git a/doc/managing_loggers.md b/doc/managing_loggers.md
index 7220af4..444eb0d 100644
--- a/doc/managing_loggers.md
+++ b/doc/managing_loggers.md
@@ -4,22 +4,22 @@ Several environment variables are used in order to adjust logger level and
 behaviour. It should be set before importing pyotb.  
 
 - `OTB_LOGGER_LEVEL` : used to set the default OTB logger level.
-- `PYOTB_LOGGER_LEVEL` : used to set the pyotb logger level. if not set, 
-- `OTB_LOGGER_LEVEL` will be used.
+- `PYOTB_LOGGER_LEVEL` : used to set the pyotb logger level.
 
+If `PYOTB_LOGGER_LEVEL` isn't set, `OTB_LOGGER_LEVEL` will be used.  
 If none of those two variables is set, the logger level will be set to 'INFO'.  
 Available levels are : DEBUG, INFO, WARNING, ERROR, CRITICAL  
 
-You may also change the logger level after import (for pyotb only) with the 
-function `set_logger_level`.
+You may also change the logger level after import (for pyotb only) 
+using pyotb.logger.setLevel(level).
 
 ```python
 import pyotb
-pyotb.set_logger_level('DEBUG')
+pyotb.logger.setLevel('DEBUG')
 ```
 
-Bonus : in some cases, you may want to silence the GDAL driver logger (for 
-example you will see a lot of errors when reading GML files with OGR).  
+Bonus : in some cases, you may want to silence the GDAL driver logger 
+(for example you will see a lot of errors when reading GML files with OGR).  
 One useful trick is to redirect these logs to a file. This can be done using 
 the variable `CPL_LOG`.
 
diff --git a/pyotb/__init__.py b/pyotb/__init__.py
index 673fd2e..7128ea6 100644
--- a/pyotb/__init__.py
+++ b/pyotb/__init__.py
@@ -1,9 +1,9 @@
 # -*- coding: utf-8 -*-
 """This module provides convenient python wrapping of otbApplications."""
-__version__ = "2.0.2"
+__version__ = "2.0.3.dev1"
 
 from .install import install_otb
-from .helpers import logger, set_logger_level
+from .helpers import logger
 from .core import (
     OTBObject,
     App,
diff --git a/pyotb/helpers.py b/pyotb/helpers.py
index 751016a..9829615 100644
--- a/pyotb/helpers.py
+++ b/pyotb/helpers.py
@@ -1,5 +1,7 @@
 """This module ensure we properly initialize pyotb, or raise SystemExit in case of broken install."""
+
 import logging
+import logging.config
 import os
 import sys
 import sysconfig
-- 
GitLab


From ef4092d27e076b5ec267604ac90738ff8dc8ec27 Mon Sep 17 00:00:00 2001
From: Vincent Delbar <vincent.delbar@gmail.com>
Date: Tue, 4 Jun 2024 16:12:23 +0200
Subject: [PATCH 05/10] CI: pylint ignore functions.py

---
 .gitlab-ci.yml | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index 715e8a3..c7015fb 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -45,7 +45,7 @@ pylint:
   before_script:
     - python3 -m pip install pylint
   script:
-    - pylint $PWD/pyotb --disable=fixme
+    - pylint $PWD/pyotb --disable=fixme --ignore=functions.py
 
 codespell:
   extends: .static_analysis
-- 
GitLab


From d2388a3bc800bd743d2f2f410353529d55f6a44d Mon Sep 17 00:00:00 2001
From: Vincent Delbar <vincent.delbar@gmail.com>
Date: Tue, 4 Jun 2024 16:30:18 +0200
Subject: [PATCH 06/10] ENH: logger levels

---
 doc/managing_loggers.md | 18 ++++++++++++++++++
 pyotb/helpers.py        |  2 +-
 2 files changed, 19 insertions(+), 1 deletion(-)

diff --git a/doc/managing_loggers.md b/doc/managing_loggers.md
index 444eb0d..20a6725 100644
--- a/doc/managing_loggers.md
+++ b/doc/managing_loggers.md
@@ -23,6 +23,24 @@ Bonus : in some cases, you may want to silence the GDAL driver logger
 One useful trick is to redirect these logs to a file. This can be done using 
 the variable `CPL_LOG`.
 
+## Log to file
+It is possible to change the behaviour of the default pyotb logger as follow
+
+```py
+import logging
+import pyotb
+# Optional : remove default stdout handler (but OTB will still print its own log)
+pyotb.logger.handlers.pop()
+# Add file handler
+handler = logging.FileHandler("/my/log/file.log")
+handler.setLevel("DEBUG")
+pyotb.logger.addHandler(handler)
+```
+
+For more advanced configuration and to manage conflicts between several loggers, 
+see the [logging module docs](https://docs.python.org/3/howto/logging-cookbook.html) 
+and use the `dictConfig()` function to configure your own logger.  
+
 ## Named applications in logs
 
 It is possible to change an app name in order to track it easily in the logs :  
diff --git a/pyotb/helpers.py b/pyotb/helpers.py
index 9829615..9653615 100644
--- a/pyotb/helpers.py
+++ b/pyotb/helpers.py
@@ -37,7 +37,7 @@ logging_cfg = {
     "handlers": {
         "stdout": {
             "class": "logging.StreamHandler",
-            "level": LOG_LEVEL,
+            "level": "DEBUG",
             "formatter": "default",
             "stream": "ext://sys.stdout",
         }
-- 
GitLab


From de801eae7e2bd80706801df4a48b23998136a5cd Mon Sep 17 00:00:00 2001
From: Vincent Delbar <vincent.delbar@gmail.com>
Date: Tue, 4 Jun 2024 16:52:59 +0200
Subject: [PATCH 07/10] CI: bump version

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

diff --git a/pyotb/__init__.py b/pyotb/__init__.py
index 7128ea6..594272b 100644
--- a/pyotb/__init__.py
+++ b/pyotb/__init__.py
@@ -1,6 +1,6 @@
 # -*- coding: utf-8 -*-
 """This module provides convenient python wrapping of otbApplications."""
-__version__ = "2.0.3.dev1"
+__version__ = "2.0.3.dev2"
 
 from .install import install_otb
 from .helpers import logger
-- 
GitLab


From d9dacde7cd79c68174d0445be196382119ab1acc Mon Sep 17 00:00:00 2001
From: Vincent Delbar <vincent.delbar@gmail.com>
Date: Tue, 4 Jun 2024 17:03:46 +0200
Subject: [PATCH 08/10] CI: add coverage config

---
 .coveragerc    | 6 ++++++
 .gitlab-ci.yml | 1 +
 2 files changed, 7 insertions(+)
 create mode 100644 .coveragerc

diff --git a/.coveragerc b/.coveragerc
new file mode 100644
index 0000000..927313b
--- /dev/null
+++ b/.coveragerc
@@ -0,0 +1,6 @@
+[run]
+omit = 
+    pyotb/depreciation.py
+    pyotb/functions.py
+    pyotb/helpers.py
+    pyotb/install.py
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index c7015fb..c932c9c 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -74,6 +74,7 @@ test_install:
     - changes:
         - "**/*.py"
         - .gitlab-ci.yml
+        - .coveragerc
   variables:
     SPOT_IMG_URL: https://gitlab.orfeo-toolbox.org/orfeotoolbox/otb/-/raw/develop/Data/Input/SP67_FR_subset_1.tif
     PLEIADES_IMG_URL: https://gitlab.orfeo-toolbox.org/orfeotoolbox/otb/-/raw/develop/Data/Baseline/OTB/Images/prTvOrthoRectification_pleiades-1_noDEM.tif
-- 
GitLab


From bc73e9c7464c281c1fc7a72097cb5c680780eb54 Mon Sep 17 00:00:00 2001
From: Vincent Delbar <vincent.delbar@gmail.com>
Date: Tue, 4 Jun 2024 17:23:10 +0200
Subject: [PATCH 09/10] CI: use forgemia stable runners tag

---
 .gitlab-ci.yml | 1 +
 1 file changed, 1 insertion(+)

diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index c932c9c..b91c24c 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -1,4 +1,5 @@
 default:
+  tags: [stable]
   image: mdl4eo/otbtf:4.3.0-cpu
   interruptible: true
 
-- 
GitLab


From f1c5ac8a9310c2db7c18ccc13002ec1f1c0293c8 Mon Sep 17 00:00:00 2001
From: Cresson Remi <remi.cresson@irstea.fr>
Date: Wed, 9 Oct 2024 19:49:49 +0200
Subject: [PATCH 10/10] Resolve "Memory leak"

---
 RELEASE_NOTES.txt |  8 ++++++--
 pyotb/__init__.py |  2 +-
 pyotb/core.py     | 28 ++++++++++++----------------
 3 files changed, 19 insertions(+), 19 deletions(-)

diff --git a/RELEASE_NOTES.txt b/RELEASE_NOTES.txt
index 7a36b43..ff64cbe 100644
--- a/RELEASE_NOTES.txt
+++ b/RELEASE_NOTES.txt
@@ -1,3 +1,9 @@
+---------------------------------------------------------------------
+2.1.0 (Oct 9, 2024) - Changes since version 2.0.2
+
+- Fix memory leak due to circular references to Output objects in list App.outputs
+- Breaking change : replaced App.outputs by a tuple of out image keys (App._out_image_keys)
+
 ---------------------------------------------------------------------
 2.0.2 (Apr 5, 2024) - Changes since version 2.0.1
 
@@ -5,13 +11,11 @@
 - Fix a bug with parameters of type "field" for vector files
 - Fix wrong output parameter key in ImageClassifier and ImageClassifierFromDeepFeatures
 
-
 ---------------------------------------------------------------------
 2.0.1 (Dec 18, 2023) - Changes since version 2.0.0
 
 - Fix a bug when writing outputs in uint8
 
-
 ---------------------------------------------------------------------
 2.0.0 (Nov 23, 2023) - Changes since version 1.5.4
 
diff --git a/pyotb/__init__.py b/pyotb/__init__.py
index 594272b..5e58316 100644
--- a/pyotb/__init__.py
+++ b/pyotb/__init__.py
@@ -1,6 +1,6 @@
 # -*- coding: utf-8 -*-
 """This module provides convenient python wrapping of otbApplications."""
-__version__ = "2.0.3.dev2"
+__version__ = "2.1.0"
 
 from .install import install_otb
 from .helpers import logger
diff --git a/pyotb/core.py b/pyotb/core.py
index b78efd4..ee73fcb 100644
--- a/pyotb/core.py
+++ b/pyotb/core.py
@@ -579,7 +579,7 @@ class App(OTBObject):
         self._exports_dic = {}
         self._settings, self._auto_parameters = {}, {}
         self._time_start, self._time_end = 0.0, 0.0
-        self.data, self.outputs = {}, {}
+        self.data = {}
         self.quiet, self.frozen = quiet, frozen
 
         # Param keys and types
@@ -597,17 +597,15 @@ class App(OTBObject):
             for key in self.parameters_keys
             if self.app.GetParameterType(key) == otb.ParameterType_Choice
         }
+        self._out_image_keys = tuple(
+            key
+            for key, param in self._out_param_types.items()
+            if param == otb.ParameterType_OutputImage
+        )
 
         # 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 (
-            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()
@@ -643,8 +641,8 @@ class App(OTBObject):
         return self._all_param_types[key] in param_types
 
     def __is_multi_output(self):
-        """Check if app has multiple outputs to ensure re-execution during write()."""
-        return len(self.outputs) > 1
+        """Check if app has multiple image outputs to ensure re-execution in write()."""
+        return len(self._out_image_keys) > 1
 
     def is_input(self, key: str) -> bool:
         """Returns True if the parameter key is an input."""
@@ -745,10 +743,8 @@ class App(OTBObject):
                     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
+            # Save / update setting value
             self._settings[key] = obj
-            if key in self.outputs:
-                self.outputs[key].filepath = obj
             if key in self._auto_parameters:
                 del self._auto_parameters[key]
 
@@ -1104,8 +1100,8 @@ class App(OTBObject):
         if isinstance(key, str):
             if key in self.data:
                 return self.data[key]
-            if key in self.outputs:
-                return self.outputs[key]
+            if key in self._out_image_keys:
+                return Output(self, key, self._settings.get(key))
             if key in self.parameters:
                 return self.parameters[key]
             raise KeyError(f"{self.name}: unknown or undefined parameter '{key}'")
@@ -1538,7 +1534,7 @@ class Output(OTBObject):
         mkdir: bool = True,
     ):
         """Constructor for an Output object, initialized during App.__init__."""
-        self.parent_pyotb_app = pyotb_app  # keep trace of parent app
+        self.parent_pyotb_app = pyotb_app  # keep a reference to parent app
         self.param_key = param_key
         self.filepath = filepath
         if mkdir and filepath is not None:
-- 
GitLab