diff --git a/.coveragerc b/.coveragerc new file mode 100644 index 0000000000000000000000000000000000000000..927313b843e2965f03890748ce23193edf43457b --- /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 715e8a36f4c45d0169c1a6eb5f27d3e9e9ca7c51..b91c24c775dfb90d491525bf125a6c19044bb5f4 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 @@ -45,7 +46,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 @@ -74,6 +75,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 diff --git a/README.md b/README.md index f5aa1841c554fd75311e815b95dd4dd3a5d2892a..93f61f51e82f83a6c6caf09e72f35a450088cfd6 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,8 @@ # pyotb: Orfeo ToolBox for Python -[](https://gitlab.orfeo-toolbox.org/nicolasnn/pyotb/-/releases) -[](https://gitlab.orfeo-toolbox.org/nicolasnn/pyotb/-/commits/develop) -[](https://gitlab.orfeo-toolbox.org/nicolasnn/pyotb/-/commits/develop) +[](https://forgemia.inra.fr/orfeo-toolbox/pyotb/-/releases) +[](https://forgemia.inra.fr/orfeo-toolbox/pyotb/-/commits/develop) +[](https://forgemia.inra.fr/orfeo-toolbox/pyotb/-/commits/develop) [](https://pyotb.readthedocs.io/en/master/) **pyotb** wraps the [Orfeo Toolbox](https://www.orfeo-toolbox.org/) in a pythonic, developer friendly diff --git a/RELEASE_NOTES.txt b/RELEASE_NOTES.txt index 7a36b430f1927e58becbe83fb8bb31adc0c3a71d..ff64cbe1e06b385e302e4cc6fbb944e7baf1809b 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/doc/index.md b/doc/index.md index 4ecfaf4462efc96263c3e39b4de2237bc6968d16..52586a42be14067af656ebd147dc204d6e848f48 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 404d7bd7d228ec6a0e850b7dcd8ba214f57b69c3..0414989a1c4f499bb07ee1840dde9f481b0ce17a 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/doc/managing_loggers.md b/doc/managing_loggers.md index 7220af482d781ad00fb62a69557def803b8d2d71..20a67257cb6756231a76e9e1b1be683062ff9232 100644 --- a/doc/managing_loggers.md +++ b/doc/managing_loggers.md @@ -4,25 +4,43 @@ 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`. +## 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/mkdocs.yml b/mkdocs.yml index 88589b11d42843d02b111f71b1d0e20546412d3e..02da75c78c363d2af9fbcf5c4100e2ace43e3918 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/ diff --git a/pyotb/__init__.py b/pyotb/__init__.py index 673fd2e9aab2f596d71050d84efcb77f9304668b..5e5831636cf19b8419a9d9c33afb655ca35fbab4 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.1.0" 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/core.py b/pyotb/core.py index b78efd478d6c42533f78996ebfba0e70ff449fea..ee73fcbcaa91754afeb41d67b26c35aa49b2274e 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: diff --git a/pyotb/helpers.py b/pyotb/helpers.py index 0e6ea2a06eee8b1b7e8cb7502d03b95861c4ef7f..96536155763a5acee7de8ef449d62c81902bd511 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 @@ -13,33 +15,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) +logger = logging.getLogger("pyotb") -def set_logger_level(level: str): - """Allow user to change the current logging level. - - 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": "DEBUG", + "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):