From f2c75140b408e68c08d5a70d64dd982f812f9da5 Mon Sep 17 00:00:00 2001 From: joeycasey87 Date: Fri, 19 Jul 2024 16:05:57 +0200 Subject: [PATCH 01/33] Added structure for the creation of postprocessing plugins --- pysteps/postprocessing/__init__.py | 1 + pysteps/postprocessing/interface.py | 172 +++++++++++++++++++++++ pysteps/postprocessing/postprocessors.py | 83 +++++++++++ 3 files changed, 256 insertions(+) create mode 100644 pysteps/postprocessing/interface.py create mode 100644 pysteps/postprocessing/postprocessors.py diff --git a/pysteps/postprocessing/__init__.py b/pysteps/postprocessing/__init__.py index 955429c92..5d17886c5 100644 --- a/pysteps/postprocessing/__init__.py +++ b/pysteps/postprocessing/__init__.py @@ -2,3 +2,4 @@ """Methods for post-processing of forecasts.""" from . import ensemblestats +from postprocessors import * \ No newline at end of file diff --git a/pysteps/postprocessing/interface.py b/pysteps/postprocessing/interface.py new file mode 100644 index 000000000..b3159c284 --- /dev/null +++ b/pysteps/postprocessing/interface.py @@ -0,0 +1,172 @@ +# -*- coding: utf-8 -*- +""" +pysteps.postprocessing.interface +==================== + +Interface for the postprocessing module. + +.. currentmodule:: pysteps.postprocessing.interface + +.. autosummary:: + :toctree: ../generated/ + + get_method +""" +import importlib + +from pkg_resources import iter_entry_points + +from pysteps import postprocessing +from pysteps.postprocessing import postprocessors +from pprint import pprint + +_postprocessor_methods = dict() + +def discover_postprocessors(): + """ + Search for installed postprocessors plugins in the entrypoint 'pysteps.plugins.postprocessors' + + The postprocessors found are added to the `pysteps.postprocessing.interface_postprocessor_methods` + dictionary containing the available postprocessors. + """ + + # The pkg resources needs to be reloaded to detect new packages installed during + # the execution of the python application. For example, when the plugins are + # installed during the tests + import pkg_resources + + importlib.reload(pkg_resources) + + for entry_point in pkg_resources.iter_entry_points(group='pysteps.plugins.postprocessors', name=None): + _postprocessor = entry_point.load() + + postprocessor_function_name = _postprocessor.__name__ + postprocessor_short_name = postprocessor_function_name.replace("postprocess_", "") + + _postprocess_kws = getattr(_postprocessor, "postprocess_kws", dict()) + _postprocessor = postprocess_import(**_postprocess_kws)(_postprocessor) + if postprocessor_short_name not in _postprocessor_methods: + _postprocessor_methods[postprocessor_short_name] = _postprocessor + else: + RuntimeWarning( + f"The postprocessor identifier '{postprocessor_short_name}' is already available in " + "'pysteps.postprocessing.interface_postprocessor_methods'.\n" + f"Skipping {entry_point.module_name}:{'.'.join(entry_point.attrs)}" + ) + + if hasattr(postprocessors, postprocessor_function_name): + RuntimeWarning( + f"The postprocessor function '{postprocessor_function_name}' is already an attribute" + "of 'pysteps.postprocessing.postprocessors'.\n" + f"Skipping {entry_point.module_name}:{'.'.join(entry_point.attrs)}" + ) + else: + setattr(postprocessors, postprocessor_function_name, _postprocessor) + +def postprocessors_info(): + """Print all the available postprocessors.""" + + # Postprocessors available in the 'postprocessing.postprocessors' module + available_postprocessors = [ + attr for attr in dir(postprocessing.postprocessors) if attr.startswith("postprocess_") + ] + + print("\nPostprocessors available in the pysteps.postprocessing.postprocessors module") + pprint(available_postprocessors) + + # Postprocessors declared in the pysteps.postprocessing.get_method interface + postprocessors_in_the_interface = [ + f.__name__ for f in postprocessing.interface._postprocessor_methods.values() + ] + + print("\nPostprocessors available in the pysteps.postprocessing.get_method interface") + pprint( + [ + (short_name, f.__name__) + for short_name, f in postprocessing.interface._postprocessor_methods.items() + ] + ) + + # Let's use sets to find out if there are postprocessors present in the postprocessor module + # but not declared in the interface, and viceversa. + available_postprocessors = set(available_postprocessors) + postprocessors_in_the_interface = set(postprocessors_in_the_interface) + + difference = available_postprocessors ^ postprocessors_in_the_interface + if len(difference) > 0: + print("\nIMPORTANT:") + _diff = available_postprocessors - postprocessors_in_the_interface + if len(_diff) > 0: + print( + "\nIMPORTANT:\nThe following postprocessors are available in pysteps.postprocessing.postprocessors module " + "but not in the pysteps.postprocessing.get_method interface" + ) + pprint(_diff) + _diff = postprocessors_in_the_interface - available_postprocessors + if len(_diff) > 0: + print( + "\nWARNING:\n" + "The following postprocessors are available in the pysteps.postprocessing.get_method " + "interface but not in the pysteps.postprocessing.postprocessors module" + ) + pprint(_diff) + + return available_postprocessors, postprocessors_in_the_interface + +def get_method(name, method_type): + """ + Return a callable function for the method corresponding to the given + name. + + Parameters + ---------- + name: str + Name of the method. The available options are:\n + + Postprocessors: + + .. tabularcolumns:: |p{2cm}|L| + + +-------------+-------------------------------------------------------+ + | Name | Description | + +=============+=======================================================+ + + method_type: {'postprocessor'} + Type of the method (see tables above). + + """ + + if isinstance(method_type, str): + method_type = method_type.lower() + else: + raise TypeError( + "Only strings supported for for the method_type" + + " argument\n" + + "The available types are: 'postprocessor'" + ) from None + + if isinstance(name, str): + name = name.lower() + else: + raise TypeError( + "Only strings supported for the method's names.\n" + + "\nAvailable postprocessors names:" + + str(list(_postprocessor_methods.keys())) + ) from None + + if method_type == "postprocessor": + methods_dict = _postprocessor_methods + else: + raise ValueError( + "Unknown method type {}\n".format(name) + + "The available types are: 'postprocessor'" + ) from None + + try: + return methods_dict[name] + except KeyError: + raise ValueError( + "Unknown {} method {}\n".format(method_type, name) + + "The available methods are:" + + str(list(methods_dict.keys())) + ) from None \ No newline at end of file diff --git a/pysteps/postprocessing/postprocessors.py b/pysteps/postprocessing/postprocessors.py new file mode 100644 index 000000000..d70d76390 --- /dev/null +++ b/pysteps/postprocessing/postprocessors.py @@ -0,0 +1,83 @@ +""" +pysteps.io.postprocessors +==================== + +Methods for applying postprocessing. + +The methods in this module implement the following interface:: + + postprocess_xxx(optional arguments) + +where **xxx** is the name of the postprocess to be applied. + +Postprocessor standardizations can be specified here if there is a desired input and output format that all should adhere to. + +Available Postprocessors +------------------------ + +.. autosummary:: + :toctree: ../generated/ + +""" + +import gzip +import os +from functools import partial + +import numpy as np + +from matplotlib.pyplot import imread + +from pysteps.decorators import postprocess_import +from pysteps.exceptions import DataModelError +from pysteps.exceptions import MissingOptionalDependency +from pysteps.utils import aggregate_fields + +try: + from osgeo import gdal, gdalconst, osr + + GDAL_IMPORTED = True +except ImportError: + GDAL_IMPORTED = False + +try: + import h5py + + H5PY_IMPORTED = True +except ImportError: + H5PY_IMPORTED = False + +try: + import metranet + + METRANET_IMPORTED = True +except ImportError: + METRANET_IMPORTED = False + +try: + import netCDF4 + + NETCDF4_IMPORTED = True +except ImportError: + NETCDF4_IMPORTED = False + +try: + from PIL import Image + + PIL_IMPORTED = True +except ImportError: + PIL_IMPORTED = False + +try: + import pyproj + + PYPROJ_IMPORTED = True +except ImportError: + PYPROJ_IMPORTED = False + +try: + import pygrib + + PYGRIB_IMPORTED = True +except ImportError: + PYGRIB_IMPORTED = False \ No newline at end of file From 11abb0d689909397011466a97ef3031bae7abeb7 Mon Sep 17 00:00:00 2001 From: joeycasey87 <139773651+joeycasey87@users.noreply.github.com> Date: Thu, 25 Jul 2024 12:06:15 +0200 Subject: [PATCH 02/33] Update interface.py --- pysteps/postprocessing/interface.py | 130 ++++++++++++++++------------ 1 file changed, 75 insertions(+), 55 deletions(-) diff --git a/pysteps/postprocessing/interface.py b/pysteps/postprocessing/interface.py index b3159c284..9d3f49657 100644 --- a/pysteps/postprocessing/interface.py +++ b/pysteps/postprocessing/interface.py @@ -16,18 +16,19 @@ from pkg_resources import iter_entry_points -from pysteps import postprocessing -from pysteps.postprocessing import postprocessors +import postprocessing +from postprocessing import diagnostics from pprint import pprint -_postprocessor_methods = dict() +_diagnostics_methods = dict() -def discover_postprocessors(): + +def discover_diagnostics(): """ - Search for installed postprocessors plugins in the entrypoint 'pysteps.plugins.postprocessors' + Search for installed diagnostics plugins in the entrypoint 'pysteps.plugins.diagnostics' - The postprocessors found are added to the `pysteps.postprocessing.interface_postprocessor_methods` - dictionary containing the available postprocessors. + The diagnostics found are added to the `pysteps.postprocessing.interface_diagnostics_methods` + dictionary containing the available diagnostics. """ # The pkg resources needs to be reloaded to detect new packages installed during @@ -37,81 +38,92 @@ def discover_postprocessors(): importlib.reload(pkg_resources) - for entry_point in pkg_resources.iter_entry_points(group='pysteps.plugins.postprocessors', name=None): - _postprocessor = entry_point.load() + for entry_point in pkg_resources.iter_entry_points( + group="pysteps.plugins.diagnostics", name=None + ): + _diagnostics = entry_point.load() - postprocessor_function_name = _postprocessor.__name__ - postprocessor_short_name = postprocessor_function_name.replace("postprocess_", "") + diagnostics_function_name = _diagnostics.__name__ + diagnostics_short_name = diagnostics_function_name.replace( + "diagnostics_", "" + ) - _postprocess_kws = getattr(_postprocessor, "postprocess_kws", dict()) - _postprocessor = postprocess_import(**_postprocess_kws)(_postprocessor) - if postprocessor_short_name not in _postprocessor_methods: - _postprocessor_methods[postprocessor_short_name] = _postprocessor + _diagnostics_kws = getattr(_diagnostics, "diagnostics_kws", dict()) + if diagnostics_short_name not in _diagnostics_methods: + _diagnostics_methods[diagnostics_short_name] = _diagnostics else: RuntimeWarning( - f"The postprocessor identifier '{postprocessor_short_name}' is already available in " - "'pysteps.postprocessing.interface_postprocessor_methods'.\n" + f"The diagnostics identifier '{diagnostics_short_name}' is already available in " + "'pysteps.postprocessing.interface_diagnostics_methods'.\n" f"Skipping {entry_point.module_name}:{'.'.join(entry_point.attrs)}" ) - if hasattr(postprocessors, postprocessor_function_name): + if hasattr(diagnostics, diagnostics_function_name): RuntimeWarning( - f"The postprocessor function '{postprocessor_function_name}' is already an attribute" - "of 'pysteps.postprocessing.postprocessors'.\n" + f"The diagnostics function '{diagnostics_function_name}' is already an attribute" + "of 'pysteps.postprocessing.diagnostics'.\n" f"Skipping {entry_point.module_name}:{'.'.join(entry_point.attrs)}" ) else: - setattr(postprocessors, postprocessor_function_name, _postprocessor) + setattr(diagnostics, diagnostics_function_name, _diagnostics) + -def postprocessors_info(): - """Print all the available postprocessors.""" +def diagnostics_info(): + """Print all the available diagnostics.""" - # Postprocessors available in the 'postprocessing.postprocessors' module - available_postprocessors = [ - attr for attr in dir(postprocessing.postprocessors) if attr.startswith("postprocess_") + # diagnostics available in the 'postprocessing.diagnostics' module + available_diagnostics = [ + attr + for attr in dir(postprocessing.diagnostics) + if attr.startswith("diagnostics") ] - print("\nPostprocessors available in the pysteps.postprocessing.postprocessors module") - pprint(available_postprocessors) + print( + "\ndiagnostics available in the pysteps.postprocessing.diagnostics module" + ) + pprint(available_diagnostics) - # Postprocessors declared in the pysteps.postprocessing.get_method interface - postprocessors_in_the_interface = [ - f.__name__ for f in postprocessing.interface._postprocessor_methods.values() + # diagnostics declared in the pysteps.postprocessing.get_method interface + diagnostics_in_the_interface = [ + f for f in list(postprocessing.interface._diagnostics_methods.keys()) ] - print("\nPostprocessors available in the pysteps.postprocessing.get_method interface") + print( + "\ndiagnostics available in the pysteps.postprocessing.get_method interface" + ) pprint( [ (short_name, f.__name__) - for short_name, f in postprocessing.interface._postprocessor_methods.items() + for short_name, f in postprocessing.interface._diagnostics_methods.items() ] ) - # Let's use sets to find out if there are postprocessors present in the postprocessor module - # but not declared in the interface, and viceversa. - available_postprocessors = set(available_postprocessors) - postprocessors_in_the_interface = set(postprocessors_in_the_interface) + # Let's use sets to find out if there are diagnostics present in the diagnostics module + # but not declared in the interface, and vice versa. + available_diagnostics = set(available_diagnostics) + diagnostics_in_the_interface = set(diagnostics_in_the_interface) - difference = available_postprocessors ^ postprocessors_in_the_interface + difference = available_diagnostics ^ diagnostics_in_the_interface if len(difference) > 0: print("\nIMPORTANT:") - _diff = available_postprocessors - postprocessors_in_the_interface + _diff = available_diagnostics - diagnostics_in_the_interface if len(_diff) > 0: print( - "\nIMPORTANT:\nThe following postprocessors are available in pysteps.postprocessing.postprocessors module " - "but not in the pysteps.postprocessing.get_method interface" + "\nIMPORTANT:\nThe following diagnostics are available in pysteps.postprocessing.diagnostics " + "module but not in the pysteps.postprocessing.get_method interface" ) pprint(_diff) - _diff = postprocessors_in_the_interface - available_postprocessors + _diff = diagnostics_in_the_interface - available_diagnostics if len(_diff) > 0: print( "\nWARNING:\n" - "The following postprocessors are available in the pysteps.postprocessing.get_method " - "interface but not in the pysteps.postprocessing.postprocessors module" + "The following diagnostics are available in the pysteps.postprocessing.get_method " + "interface but not in the pysteps.postprocessing.diagnostics module" ) pprint(_diff) - return available_postprocessors, postprocessors_in_the_interface + return available_diagnostics, diagnostics_in_the_interface + def get_method(name, method_type): """ @@ -123,7 +135,7 @@ def get_method(name, method_type): name: str Name of the method. The available options are:\n - Postprocessors: + diagnostics: .. tabularcolumns:: |p{2cm}|L| @@ -131,7 +143,15 @@ def get_method(name, method_type): | Name | Description | +=============+=======================================================+ - method_type: {'postprocessor'} + Diagnostic diagnostics: + + .. tabularcolumns:: |p{2cm}|L| + + +-------------+-------------------------------------------------------+ + | Name | Description | + +=============+=======================================================+ + + method_type: {'diagnostics', diagnostic_diagnostics} Type of the method (see tables above). """ @@ -142,7 +162,7 @@ def get_method(name, method_type): raise TypeError( "Only strings supported for for the method_type" + " argument\n" - + "The available types are: 'postprocessor'" + + "The available types are: 'diagnostics'" ) from None if isinstance(name, str): @@ -150,16 +170,16 @@ def get_method(name, method_type): else: raise TypeError( "Only strings supported for the method's names.\n" - + "\nAvailable postprocessors names:" - + str(list(_postprocessor_methods.keys())) + + "\nAvailable diagnostics names:" + + str(list(_diagnostics_methods.keys())) ) from None - if method_type == "postprocessor": - methods_dict = _postprocessor_methods + if method_type == "diagnostics": + methods_dict = _diagnostics_methods else: raise ValueError( - "Unknown method type {}\n".format(name) - + "The available types are: 'postprocessor'" + "Unknown method type {}\n".format(method_type) + + "The available types are: 'diagnostics'" ) from None try: @@ -169,4 +189,4 @@ def get_method(name, method_type): "Unknown {} method {}\n".format(method_type, name) + "The available methods are:" + str(list(methods_dict.keys())) - ) from None \ No newline at end of file + ) from None From 36741db2690c1bda519a637eb9c926d281df5776 Mon Sep 17 00:00:00 2001 From: joeycasey87 Date: Thu, 25 Jul 2024 13:47:29 +0200 Subject: [PATCH 03/33] Updated to fit black formatting --- pysteps/postprocessing/__init__.py | 3 ++- .../{postprocessors.py => diagnostics.py} | 7 ++++--- pysteps/postprocessing/interface.py | 14 ++++---------- 3 files changed, 10 insertions(+), 14 deletions(-) rename pysteps/postprocessing/{postprocessors.py => diagnostics.py} (91%) diff --git a/pysteps/postprocessing/__init__.py b/pysteps/postprocessing/__init__.py index 5d17886c5..14da1c6bb 100644 --- a/pysteps/postprocessing/__init__.py +++ b/pysteps/postprocessing/__init__.py @@ -2,4 +2,5 @@ """Methods for post-processing of forecasts.""" from . import ensemblestats -from postprocessors import * \ No newline at end of file +from .diagnostics import * +from .interface import * diff --git a/pysteps/postprocessing/postprocessors.py b/pysteps/postprocessing/diagnostics.py similarity index 91% rename from pysteps/postprocessing/postprocessors.py rename to pysteps/postprocessing/diagnostics.py index d70d76390..973f63301 100644 --- a/pysteps/postprocessing/postprocessors.py +++ b/pysteps/postprocessing/diagnostics.py @@ -1,5 +1,5 @@ """ -pysteps.io.postprocessors +pysteps.postprocessing.diagnostics ==================== Methods for applying postprocessing. @@ -10,7 +10,8 @@ where **xxx** is the name of the postprocess to be applied. -Postprocessor standardizations can be specified here if there is a desired input and output format that all should adhere to. +Postprocessor standardizations can be specified here if there is a desired input and output format that all should +adhere to. Available Postprocessors ------------------------ @@ -80,4 +81,4 @@ PYGRIB_IMPORTED = True except ImportError: - PYGRIB_IMPORTED = False \ No newline at end of file + PYGRIB_IMPORTED = False diff --git a/pysteps/postprocessing/interface.py b/pysteps/postprocessing/interface.py index 9d3f49657..a52a2ebcc 100644 --- a/pysteps/postprocessing/interface.py +++ b/pysteps/postprocessing/interface.py @@ -39,14 +39,12 @@ def discover_diagnostics(): importlib.reload(pkg_resources) for entry_point in pkg_resources.iter_entry_points( - group="pysteps.plugins.diagnostics", name=None + group="pysteps.plugins.diagnostics", name=None ): _diagnostics = entry_point.load() diagnostics_function_name = _diagnostics.__name__ - diagnostics_short_name = diagnostics_function_name.replace( - "diagnostics_", "" - ) + diagnostics_short_name = diagnostics_function_name.replace("diagnostics_", "") _diagnostics_kws = getattr(_diagnostics, "diagnostics_kws", dict()) if diagnostics_short_name not in _diagnostics_methods: @@ -78,9 +76,7 @@ def diagnostics_info(): if attr.startswith("diagnostics") ] - print( - "\ndiagnostics available in the pysteps.postprocessing.diagnostics module" - ) + print("\ndiagnostics available in the pysteps.postprocessing.diagnostics module") pprint(available_diagnostics) # diagnostics declared in the pysteps.postprocessing.get_method interface @@ -88,9 +84,7 @@ def diagnostics_info(): f for f in list(postprocessing.interface._diagnostics_methods.keys()) ] - print( - "\ndiagnostics available in the pysteps.postprocessing.get_method interface" - ) + print("\ndiagnostics available in the pysteps.postprocessing.get_method interface") pprint( [ (short_name, f.__name__) From 9e79882ca47b2dbe5be55912128ddd4bdb76cbfd Mon Sep 17 00:00:00 2001 From: joeycasey87 Date: Thu, 25 Jul 2024 13:54:57 +0200 Subject: [PATCH 04/33] Update interface.py --- pysteps/postprocessing/interface.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/pysteps/postprocessing/interface.py b/pysteps/postprocessing/interface.py index a52a2ebcc..ccd4193ed 100644 --- a/pysteps/postprocessing/interface.py +++ b/pysteps/postprocessing/interface.py @@ -16,8 +16,8 @@ from pkg_resources import iter_entry_points -import postprocessing -from postprocessing import diagnostics +import pysteps.postprocessing +from pysteps.postprocessing import diagnostics from pprint import pprint _diagnostics_methods = dict() @@ -72,7 +72,7 @@ def diagnostics_info(): # diagnostics available in the 'postprocessing.diagnostics' module available_diagnostics = [ attr - for attr in dir(postprocessing.diagnostics) + for attr in dir(pysteps.postprocessing.diagnostics) if attr.startswith("diagnostics") ] @@ -81,14 +81,14 @@ def diagnostics_info(): # diagnostics declared in the pysteps.postprocessing.get_method interface diagnostics_in_the_interface = [ - f for f in list(postprocessing.interface._diagnostics_methods.keys()) + f for f in list(pysteps.postprocessing.interface._diagnostics_methods.keys()) ] print("\ndiagnostics available in the pysteps.postprocessing.get_method interface") pprint( [ (short_name, f.__name__) - for short_name, f in postprocessing.interface._diagnostics_methods.items() + for short_name, f in pysteps.postprocessing.interface._diagnostics_methods.items() ] ) From 5abe46716356f69ec9ca6a7b02fecb51d7c24401 Mon Sep 17 00:00:00 2001 From: joeycasey87 Date: Thu, 25 Jul 2024 14:27:49 +0200 Subject: [PATCH 05/33] Update diagnostics.py --- pysteps/postprocessing/diagnostics.py | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/pysteps/postprocessing/diagnostics.py b/pysteps/postprocessing/diagnostics.py index 973f63301..426a25f59 100644 --- a/pysteps/postprocessing/diagnostics.py +++ b/pysteps/postprocessing/diagnostics.py @@ -21,19 +21,6 @@ """ -import gzip -import os -from functools import partial - -import numpy as np - -from matplotlib.pyplot import imread - -from pysteps.decorators import postprocess_import -from pysteps.exceptions import DataModelError -from pysteps.exceptions import MissingOptionalDependency -from pysteps.utils import aggregate_fields - try: from osgeo import gdal, gdalconst, osr From 092557c99a9cd1ba1c8286ca57670407b9abdcff Mon Sep 17 00:00:00 2001 From: joeycasey87 Date: Tue, 13 Aug 2024 15:01:04 +0200 Subject: [PATCH 06/33] Added tests to improve code coverage Also reformatted with black. Tests were added for the get_method and diagnostics_info functions in the postprocessing interface. Tests for the discover_diagnostics function will be written once these changes have been merged as then the cookiecutter plugins can then be properly tested. --- pysteps/postprocessing/diagnostics.py | 55 ++++----------------------- pysteps/postprocessing/interface.py | 8 +++- pysteps/tests/test_interfaces.py | 48 +++++++++++++++++++++++ 3 files changed, 61 insertions(+), 50 deletions(-) diff --git a/pysteps/postprocessing/diagnostics.py b/pysteps/postprocessing/diagnostics.py index 426a25f59..a6f9f7c85 100644 --- a/pysteps/postprocessing/diagnostics.py +++ b/pysteps/postprocessing/diagnostics.py @@ -2,13 +2,13 @@ pysteps.postprocessing.diagnostics ==================== -Methods for applying postprocessing. +Methods for diagnostic postprocessing. The methods in this module implement the following interface:: - postprocess_xxx(optional arguments) + diagnostic_xyz(optional arguments) -where **xxx** is the name of the postprocess to be applied. +where **xyz** is the name of the diagnostic postprocessing to be applied. Postprocessor standardizations can be specified here if there is a desired input and output format that all should adhere to. @@ -21,51 +21,10 @@ """ -try: - from osgeo import gdal, gdalconst, osr - GDAL_IMPORTED = True -except ImportError: - GDAL_IMPORTED = False +def diagnostics_example1(filename, **kwargs): + return "Hello, I am an example postprocessor." -try: - import h5py - H5PY_IMPORTED = True -except ImportError: - H5PY_IMPORTED = False - -try: - import metranet - - METRANET_IMPORTED = True -except ImportError: - METRANET_IMPORTED = False - -try: - import netCDF4 - - NETCDF4_IMPORTED = True -except ImportError: - NETCDF4_IMPORTED = False - -try: - from PIL import Image - - PIL_IMPORTED = True -except ImportError: - PIL_IMPORTED = False - -try: - import pyproj - - PYPROJ_IMPORTED = True -except ImportError: - PYPROJ_IMPORTED = False - -try: - import pygrib - - PYGRIB_IMPORTED = True -except ImportError: - PYGRIB_IMPORTED = False +def diagnostics_example2(filename, **kwargs): + return 42 diff --git a/pysteps/postprocessing/interface.py b/pysteps/postprocessing/interface.py index ccd4193ed..422ac78e1 100644 --- a/pysteps/postprocessing/interface.py +++ b/pysteps/postprocessing/interface.py @@ -20,7 +20,9 @@ from pysteps.postprocessing import diagnostics from pprint import pprint -_diagnostics_methods = dict() +_diagnostics_methods = dict( + example1=diagnostics.diagnostics_example1, example3=lambda x: [x, x] +) def discover_diagnostics(): @@ -97,6 +99,8 @@ def diagnostics_info(): available_diagnostics = set(available_diagnostics) diagnostics_in_the_interface = set(diagnostics_in_the_interface) + available_diagnostics = {s.split("_")[1] for s in available_diagnostics} + difference = available_diagnostics ^ diagnostics_in_the_interface if len(difference) > 0: print("\nIMPORTANT:") @@ -145,7 +149,7 @@ def get_method(name, method_type): | Name | Description | +=============+=======================================================+ - method_type: {'diagnostics', diagnostic_diagnostics} + method_type: {'diagnostics', diagnostics_name} Type of the method (see tables above). """ diff --git a/pysteps/tests/test_interfaces.py b/pysteps/tests/test_interfaces.py index c490b2539..bec4338f6 100644 --- a/pysteps/tests/test_interfaces.py +++ b/pysteps/tests/test_interfaces.py @@ -360,3 +360,51 @@ def test_tracking_interface(): invalid_names = ["lucas-kanade", "dating"] _generic_interface_test(method_getter, valid_names_func_pair, invalid_names) + + +def test_postprocessing_interface(): + """Test the postprocessing module interface.""" + + import pysteps.postprocessing as postprocessing + from pysteps.postprocessing import diagnostics_example1 + from pysteps.postprocessing import diagnostics_example2 + + valid_names_func_pair = [("example1", diagnostics_example1)] + + def method_getter(name): + return pysteps.postprocessing.interface.get_method(name, "diagnostics") + + invalid_names = ["bom", "fmi", "knmi", "mch", "mrms", "opera", "saf"] + _generic_interface_test(method_getter, valid_names_func_pair, invalid_names) + + # Test for invalid argument type + with pytest.raises(TypeError): + pysteps.postprocessing.interface.get_method("example1", None) + pysteps.postprocessing.interface.get_method(None, "diagnostics") + + # Test for invalid method types + with pytest.raises(ValueError): + pysteps.postprocessing.interface.get_method("example1", "io") + + with pytest.raises(TypeError): + pysteps.postprocessing.interface.get_method(24, "diagnostics") + + assert isinstance( + pysteps.postprocessing.interface.diagnostics_info()[0], + set, + ) + + assert isinstance( + pysteps.postprocessing.interface.diagnostics_info()[1], + set, + ) + + assert isinstance( + diagnostics_example1(filename="example_filename"), + str, + ) + + assert isinstance( + diagnostics_example2(filename="example_filename"), + int, + ) From 15de2a435b9d37cfd4e1d03df8c9fb61b8c2d87d Mon Sep 17 00:00:00 2001 From: Lesley De Cruz Date: Tue, 20 Aug 2024 17:24:28 +0200 Subject: [PATCH 07/33] Remove redundant import and getattr statements --- pysteps/postprocessing/interface.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/pysteps/postprocessing/interface.py b/pysteps/postprocessing/interface.py index 422ac78e1..e25507624 100644 --- a/pysteps/postprocessing/interface.py +++ b/pysteps/postprocessing/interface.py @@ -14,8 +14,6 @@ """ import importlib -from pkg_resources import iter_entry_points - import pysteps.postprocessing from pysteps.postprocessing import diagnostics from pprint import pprint @@ -48,7 +46,6 @@ def discover_diagnostics(): diagnostics_function_name = _diagnostics.__name__ diagnostics_short_name = diagnostics_function_name.replace("diagnostics_", "") - _diagnostics_kws = getattr(_diagnostics, "diagnostics_kws", dict()) if diagnostics_short_name not in _diagnostics_methods: _diagnostics_methods[diagnostics_short_name] = _diagnostics else: From c6ca85d03dc7844feba7bb74c15f3547d7242ad0 Mon Sep 17 00:00:00 2001 From: joeycasey87 Date: Mon, 26 Aug 2024 10:22:00 +0200 Subject: [PATCH 08/33] Adjusted so that the interface is more easily interpretable --- .github/workflows/check_black.yml | 4 +- .github/workflows/test_pysteps.yml | 4 +- pysteps/postprocessing/__init__.py | 1 + pysteps/postprocessing/diagnostics.py | 68 ++++++-- pysteps/postprocessing/ensemblestats.py | 8 + pysteps/postprocessing/interface.py | 198 +++++++++++++++++++----- pysteps/tests/test_interfaces.py | 124 +++++++++------ 7 files changed, 302 insertions(+), 105 deletions(-) diff --git a/.github/workflows/check_black.yml b/.github/workflows/check_black.yml index 41e1ca6ff..c8360540c 100644 --- a/.github/workflows/check_black.yml +++ b/.github/workflows/check_black.yml @@ -11,9 +11,9 @@ name: Check Black on: # Triggers the workflow on push or pull request events but only for the master branch push: - branches: [ master ] + branches: [ main ] pull_request: - branches: [ master ] + branches: [ main ] jobs: build: diff --git a/.github/workflows/test_pysteps.yml b/.github/workflows/test_pysteps.yml index 065c28b30..fd5642cce 100644 --- a/.github/workflows/test_pysteps.yml +++ b/.github/workflows/test_pysteps.yml @@ -4,11 +4,11 @@ on: # Triggers the workflow on push or pull request events to the master branch push: branches: - - master + - main - pysteps-v2 pull_request: branches: - - master + - main - pysteps-v2 jobs: diff --git a/pysteps/postprocessing/__init__.py b/pysteps/postprocessing/__init__.py index 14da1c6bb..d11c33cab 100644 --- a/pysteps/postprocessing/__init__.py +++ b/pysteps/postprocessing/__init__.py @@ -4,3 +4,4 @@ from . import ensemblestats from .diagnostics import * from .interface import * +from .ensemblestats import * diff --git a/pysteps/postprocessing/diagnostics.py b/pysteps/postprocessing/diagnostics.py index a6f9f7c85..7f0ad8790 100644 --- a/pysteps/postprocessing/diagnostics.py +++ b/pysteps/postprocessing/diagnostics.py @@ -2,18 +2,15 @@ pysteps.postprocessing.diagnostics ==================== -Methods for diagnostic postprocessing. +Methods for applying diagnostics postprocessing. The methods in this module implement the following interface:: - diagnostic_xyz(optional arguments) + diagnostics_xxx(optional arguments) -where **xyz** is the name of the diagnostic postprocessing to be applied. +where **xxx** is the name of the diagnostic to be applied. -Postprocessor standardizations can be specified here if there is a desired input and output format that all should -adhere to. - -Available Postprocessors +Available Diagnostics Postprocessors ------------------------ .. autosummary:: @@ -21,10 +18,59 @@ """ +try: + from osgeo import gdal, gdalconst, osr + + GDAL_IMPORTED = True +except ImportError: + GDAL_IMPORTED = False + +try: + import h5py + + H5PY_IMPORTED = True +except ImportError: + H5PY_IMPORTED = False + +try: + import metranet + + METRANET_IMPORTED = True +except ImportError: + METRANET_IMPORTED = False + +try: + import netCDF4 + + NETCDF4_IMPORTED = True +except ImportError: + NETCDF4_IMPORTED = False + +try: + from PIL import Image + + PIL_IMPORTED = True +except ImportError: + PIL_IMPORTED = False + +try: + import pyproj + + PYPROJ_IMPORTED = True +except ImportError: + PYPROJ_IMPORTED = False + +try: + import pygrib + + PYGRIB_IMPORTED = True +except ImportError: + PYGRIB_IMPORTED = False + -def diagnostics_example1(filename, **kwargs): - return "Hello, I am an example postprocessor." +def postprocessors_diagnostics_example1(filename, **kwargs): + return "Hello, I am an example diagnostics postprocessor." -def diagnostics_example2(filename, **kwargs): - return 42 +def postprocessors_diagnostics_example2(filename, **kwargs): + return [[42, 42], [42, 42]] diff --git a/pysteps/postprocessing/ensemblestats.py b/pysteps/postprocessing/ensemblestats.py index 351ec4a31..21323f9bd 100644 --- a/pysteps/postprocessing/ensemblestats.py +++ b/pysteps/postprocessing/ensemblestats.py @@ -177,3 +177,11 @@ def banddepth(X, thr=None, norm=False): depth = (depth - depth.min()) / (depth.max() - depth.min()) return depth + + +def postprocessors_ensemblestats_example1(filename, **kwargs): + return "Hello, I am an example of postprocessing ensemble statistics." + + +def postprocessors_ensemblestats_example2(filename, **kwargs): + return [[42, 42], [42, 42]] diff --git a/pysteps/postprocessing/interface.py b/pysteps/postprocessing/interface.py index e25507624..52f0b912a 100644 --- a/pysteps/postprocessing/interface.py +++ b/pysteps/postprocessing/interface.py @@ -15,20 +15,26 @@ import importlib import pysteps.postprocessing -from pysteps.postprocessing import diagnostics +from pysteps.postprocessing import diagnostics, ensemblestats from pprint import pprint _diagnostics_methods = dict( - example1=diagnostics.diagnostics_example1, example3=lambda x: [x, x] + diagnostics_example1=diagnostics.postprocessors_diagnostics_example1, + diagnostics_example3=lambda x: [x, x], +) + +_ensemblestats_methods = dict( + ensemblestats_example1=ensemblestats.postprocessors_ensemblestats_example1, + ensemblestats_example3=lambda x, y: [x, y], ) -def discover_diagnostics(): +def discover_postprocessors(): """ - Search for installed diagnostics plugins in the entrypoint 'pysteps.plugins.diagnostics' + Search for installed postprocessing plugins in the entrypoint 'pysteps.plugins.postprocessors' - The diagnostics found are added to the `pysteps.postprocessing.interface_diagnostics_methods` - dictionary containing the available diagnostics. + The postprocessors found are added to the appropriate `_methods` + dictionary in 'pysteps.postprocessing.interface' containing the available postprocessors. """ # The pkg resources needs to be reloaded to detect new packages installed during @@ -39,13 +45,16 @@ def discover_diagnostics(): importlib.reload(pkg_resources) for entry_point in pkg_resources.iter_entry_points( - group="pysteps.plugins.diagnostics", name=None + group="pysteps.plugins.postprocessors", name=None ): - _diagnostics = entry_point.load() + _postprocessors = entry_point.load() - diagnostics_function_name = _diagnostics.__name__ - diagnostics_short_name = diagnostics_function_name.replace("diagnostics_", "") + postprocessors_function_name = _postprocessors.__name__ + postprocessors_short_name = postprocessors_function_name.replace( + "postprocessors_", "" + ) +<<<<<<< Updated upstream if diagnostics_short_name not in _diagnostics_methods: _diagnostics_methods[diagnostics_short_name] = _diagnostics else: @@ -53,32 +62,67 @@ def discover_diagnostics(): f"The diagnostics identifier '{diagnostics_short_name}' is already available in " "'pysteps.postprocessing.interface_diagnostics_methods'.\n" f"Skipping {entry_point.module_name}:{'.'.join(entry_point.attrs)}" +======= + if postprocessors_short_name.startswith("diagnostics_"): + diagnostics_short_name = postprocessors_short_name.replace( + "diagnostics_", "" +>>>>>>> Stashed changes ) - - if hasattr(diagnostics, diagnostics_function_name): - RuntimeWarning( - f"The diagnostics function '{diagnostics_function_name}' is already an attribute" - "of 'pysteps.postprocessing.diagnostics'.\n" - f"Skipping {entry_point.module_name}:{'.'.join(entry_point.attrs)}" + if diagnostics_short_name not in _diagnostics_methods: + _diagnostics_methods[diagnostics_short_name] = _postprocessors + else: + RuntimeWarning( + f"The diagnostics identifier '{diagnostics_short_name}' is already available in " + "'pysteps.postprocessing.interface_diagnostics_methods'.\n" + f"Skipping {entry_point.module_name}:{'.'.join(entry_point.attrs)}" + ) + + if hasattr(diagnostics, postprocessors_short_name): + RuntimeWarning( + f"The diagnostics function '{diagnostics_short_name}' is already an attribute" + "of 'pysteps.postprocessing.diagnostics'.\n" + f"Skipping {entry_point.module_name}:{'.'.join(entry_point.attrs)}" + ) + else: + setattr(diagnostics, postprocessors_function_name, _postprocessors) + + elif postprocessors_short_name.startswith("ensemblestats_"): + ensemblestats_short_name = postprocessors_short_name.replace( + "ensemblestats_", "" ) - else: - setattr(diagnostics, diagnostics_function_name, _diagnostics) - - -def diagnostics_info(): - """Print all the available diagnostics.""" + if ensemblestats_short_name not in _ensemblestats_methods: + _ensemblestats_methods[ensemblestats_short_name] = _postprocessors + else: + RuntimeWarning( + f"The ensemblestats identifier '{ensemblestats_short_name}' is already available in " + "'pysteps.postprocessing.interface_ensemblestats_methods'.\n" + f"Skipping {entry_point.module_name}:{'.'.join(entry_point.attrs)}" + ) + + if hasattr(ensemblestats, postprocessors_short_name): + RuntimeWarning( + f"The ensemblestats function '{ensemblestats_short_name}' is already an attribute" + "of 'pysteps.postprocessing.diagnostics'.\n" + f"Skipping {entry_point.module_name}:{'.'.join(entry_point.attrs)}" + ) + else: + setattr(ensemblestats, postprocessors_function_name, _postprocessors) + + +def postprocessors_info(): + """Print all the available postprocessors.""" # diagnostics available in the 'postprocessing.diagnostics' module available_diagnostics = [ attr for attr in dir(pysteps.postprocessing.diagnostics) - if attr.startswith("diagnostics") + if attr.startswith("postprocessors") ] - print("\ndiagnostics available in the pysteps.postprocessing.diagnostics module") + print("\npostprocessors available in the pysteps.postprocessing.diagnostics module") pprint(available_diagnostics) - # diagnostics declared in the pysteps.postprocessing.get_method interface + # diagnostics postprocessors declared in the pysteps.postprocessing.get_method interface diagnostics_in_the_interface = [ f for f in list(pysteps.postprocessing.interface._diagnostics_methods.keys()) ] @@ -91,20 +135,18 @@ def diagnostics_info(): ] ) - # Let's use sets to find out if there are diagnostics present in the diagnostics module + # Let's use sets to find out if there are postprocessors present in the diagnostics module # but not declared in the interface, and vice versa. available_diagnostics = set(available_diagnostics) diagnostics_in_the_interface = set(diagnostics_in_the_interface) - available_diagnostics = {s.split("_")[1] for s in available_diagnostics} - difference = available_diagnostics ^ diagnostics_in_the_interface if len(difference) > 0: print("\nIMPORTANT:") _diff = available_diagnostics - diagnostics_in_the_interface if len(_diff) > 0: print( - "\nIMPORTANT:\nThe following diagnostics are available in pysteps.postprocessing.diagnostics " + "\nIMPORTANT:\nThe following postprocessors are available in pysteps.postprocessing.diagnostics " "module but not in the pysteps.postprocessing.get_method interface" ) pprint(_diff) @@ -112,12 +154,68 @@ def diagnostics_info(): if len(_diff) > 0: print( "\nWARNING:\n" - "The following diagnostics are available in the pysteps.postprocessing.get_method " + "The following postprocessors are available in the pysteps.postprocessing.get_method " "interface but not in the pysteps.postprocessing.diagnostics module" ) pprint(_diff) - return available_diagnostics, diagnostics_in_the_interface + # postprocessors available in the 'postprocessing.ensemblestats' module + available_ensemblestats = [ + attr + for attr in dir(pysteps.postprocessing.ensemblestats) + if attr.startswith("postprocessors") + ] + + print( + "\npostprocessors available in the pysteps.postprocessing.ensemblestats module" + ) + pprint(available_ensemblestats) + + # ensemblestats postprocessors declared in the pysteps.postprocessing.get_method interface + ensemblestats_in_the_interface = [ + f for f in list(pysteps.postprocessing.interface._ensemblestats_methods.keys()) + ] + + print( + "\npostprocessors available in the pysteps.postprocessing.get_method interface" + ) + pprint( + [ + (short_name, f.__name__) + for short_name, f in pysteps.postprocessing.interface._ensemblestats_methods.items() + ] + ) + + # Let's use sets to find out if there are postprocessors present in the ensemblestats module + # but not declared in the interface, and vice versa. + available_ensemblestats = set(available_ensemblestats) + ensemblestats_in_the_interface = set(ensemblestats_in_the_interface) + + difference = available_ensemblestats ^ ensemblestats_in_the_interface + if len(difference) > 0: + print("\nIMPORTANT:") + _diff = available_ensemblestats - ensemblestats_in_the_interface + if len(_diff) > 0: + print( + "\nIMPORTANT:\nThe following postprocessors are available in pysteps.postprocessing.ensemblestats " + "module but not in the pysteps.postprocessing.get_method interface" + ) + pprint(_diff) + _diff = ensemblestats_in_the_interface - available_ensemblestats + if len(_diff) > 0: + print( + "\nWARNING:\n" + "The following postprocessors are available in the pysteps.postprocessing.get_method " + "interface but not in the pysteps.postprocessing.ensemblestats module" + ) + pprint(_diff) + + available_postprocessors = available_diagnostics.union(available_ensemblestats) + postprocessors_in_the_interface = ensemblestats_in_the_interface.union( + diagnostics_in_the_interface + ) + + return available_postprocessors, postprocessors_in_the_interface def get_method(name, method_type): @@ -134,19 +232,31 @@ def get_method(name, method_type): .. tabularcolumns:: |p{2cm}|L| - +-------------+-------------------------------------------------------+ - | Name | Description | - +=============+=======================================================+ + +---------------+-------------------------------------------------------+ + | Name | Description | + +===============+=======================================================+ + | Diagnostics | Example that returns a string | + | Example1 | | + +---------------+-------------------------------------------------------+ + | Diagnostics | Example that returns an array | + | Example3 | | + +---------------+-------------------------------------------------------+ - Diagnostic diagnostics: + ensemblestats: .. tabularcolumns:: |p{2cm}|L| - +-------------+-------------------------------------------------------+ - | Name | Description | - +=============+=======================================================+ - - method_type: {'diagnostics', diagnostics_name} + +---------------+-------------------------------------------------------+ + | Name | Description | + +===============+=======================================================+ + | EnsembleStats | Example that returns a string | + | Example1 | | + +---------------+-------------------------------------------------------+ + | EnsembleStats | Example that returns an array | + | Example3 | | + +---------------+-------------------------------------------------------+ + + method_type: {'diagnostics', 'ensemblestats'} Type of the method (see tables above). """ @@ -157,7 +267,7 @@ def get_method(name, method_type): raise TypeError( "Only strings supported for for the method_type" + " argument\n" - + "The available types are: 'diagnostics'" + + "The available types are: 'diagnostics', 'ensemblestats'" ) from None if isinstance(name, str): @@ -167,14 +277,18 @@ def get_method(name, method_type): "Only strings supported for the method's names.\n" + "\nAvailable diagnostics names:" + str(list(_diagnostics_methods.keys())) + + "\nAvailable ensemblestats names:" + + str(list(_ensemblestats_methods.keys())) ) from None if method_type == "diagnostics": methods_dict = _diagnostics_methods + elif method_type == "ensemblestats": + methods_dict = _ensemblestats_methods else: raise ValueError( "Unknown method type {}\n".format(method_type) - + "The available types are: 'diagnostics'" + + "The available types are: 'diagnostics', 'ensemblestats'" ) from None try: diff --git a/pysteps/tests/test_interfaces.py b/pysteps/tests/test_interfaces.py index bec4338f6..f652816a7 100644 --- a/pysteps/tests/test_interfaces.py +++ b/pysteps/tests/test_interfaces.py @@ -152,6 +152,82 @@ def method_getter(name): pysteps.io.interface.get_method("mch_gif", "io") +def test_postprocessing_interface(): + """Test the postprocessing module interface.""" + + import pysteps.postprocessing as postprocessing + from pysteps.postprocessing import postprocessors_diagnostics_example1 + from pysteps.postprocessing import postprocessors_diagnostics_example2 + from pysteps.postprocessing import postprocessors_ensemblestats_example1 + from pysteps.postprocessing import postprocessors_ensemblestats_example2 + + valid_diagnostics_names_func_pair = [ + ("diagnostics_example1", postprocessors_diagnostics_example1) + ] + + def method_getter_diagnostics(name): + return pysteps.postprocessing.interface.get_method(name, "diagnostics") + + invalid_names = ["bom", "fmi", "knmi", "mch", "mrms", "opera", "saf"] + _generic_interface_test( + method_getter_diagnostics, valid_diagnostics_names_func_pair, invalid_names + ) + + valid_ensemblestats_names_func_pair = [ + ("ensemblestats_example1", postprocessors_ensemblestats_example1) + ] + + def method_getter_ensemblestats(name): + return pysteps.postprocessing.interface.get_method(name, "ensemblestats") + + invalid_names = ["bom", "fmi", "knmi", "mch", "mrms", "opera", "saf"] + _generic_interface_test( + method_getter_ensemblestats, valid_ensemblestats_names_func_pair, invalid_names + ) + + # Test for invalid argument type + with pytest.raises(TypeError): + pysteps.postprocessing.interface.get_method("diagnostics_example1", None) + pysteps.postprocessing.interface.get_method(None, "diagnostics") + + # Test for invalid method types + with pytest.raises(ValueError): + pysteps.postprocessing.interface.get_method("diagnostics_example1", "io") + + with pytest.raises(TypeError): + pysteps.postprocessing.interface.get_method(24, "diagnostics") + + assert isinstance( + pysteps.postprocessing.interface.postprocessors_info()[0], + set, + ) + + assert isinstance( + pysteps.postprocessing.interface.postprocessors_info()[1], + set, + ) + + assert isinstance( + postprocessors_diagnostics_example1(filename="example_filename"), + str, + ) + + assert isinstance( + postprocessors_diagnostics_example2(filename="example_filename"), + list, + ) + + assert isinstance( + postprocessors_ensemblestats_example1(filename="example_filename"), + str, + ) + + assert isinstance( + postprocessors_ensemblestats_example2(filename="example_filename"), + list, + ) + + def test_motion_interface(): """Test the motion module interface.""" @@ -360,51 +436,3 @@ def test_tracking_interface(): invalid_names = ["lucas-kanade", "dating"] _generic_interface_test(method_getter, valid_names_func_pair, invalid_names) - - -def test_postprocessing_interface(): - """Test the postprocessing module interface.""" - - import pysteps.postprocessing as postprocessing - from pysteps.postprocessing import diagnostics_example1 - from pysteps.postprocessing import diagnostics_example2 - - valid_names_func_pair = [("example1", diagnostics_example1)] - - def method_getter(name): - return pysteps.postprocessing.interface.get_method(name, "diagnostics") - - invalid_names = ["bom", "fmi", "knmi", "mch", "mrms", "opera", "saf"] - _generic_interface_test(method_getter, valid_names_func_pair, invalid_names) - - # Test for invalid argument type - with pytest.raises(TypeError): - pysteps.postprocessing.interface.get_method("example1", None) - pysteps.postprocessing.interface.get_method(None, "diagnostics") - - # Test for invalid method types - with pytest.raises(ValueError): - pysteps.postprocessing.interface.get_method("example1", "io") - - with pytest.raises(TypeError): - pysteps.postprocessing.interface.get_method(24, "diagnostics") - - assert isinstance( - pysteps.postprocessing.interface.diagnostics_info()[0], - set, - ) - - assert isinstance( - pysteps.postprocessing.interface.diagnostics_info()[1], - set, - ) - - assert isinstance( - diagnostics_example1(filename="example_filename"), - str, - ) - - assert isinstance( - diagnostics_example2(filename="example_filename"), - int, - ) From f5558acbbb1e4f92c2748a2bf72140bb6a0019fb Mon Sep 17 00:00:00 2001 From: joeycasey87 Date: Mon, 26 Aug 2024 10:30:09 +0200 Subject: [PATCH 09/33] Fixed error from stashed chages --- pysteps/postprocessing/interface.py | 12 +----------- 1 file changed, 1 insertion(+), 11 deletions(-) diff --git a/pysteps/postprocessing/interface.py b/pysteps/postprocessing/interface.py index 52f0b912a..eef6a95df 100644 --- a/pysteps/postprocessing/interface.py +++ b/pysteps/postprocessing/interface.py @@ -54,19 +54,9 @@ def discover_postprocessors(): "postprocessors_", "" ) -<<<<<<< Updated upstream - if diagnostics_short_name not in _diagnostics_methods: - _diagnostics_methods[diagnostics_short_name] = _diagnostics - else: - RuntimeWarning( - f"The diagnostics identifier '{diagnostics_short_name}' is already available in " - "'pysteps.postprocessing.interface_diagnostics_methods'.\n" - f"Skipping {entry_point.module_name}:{'.'.join(entry_point.attrs)}" -======= if postprocessors_short_name.startswith("diagnostics_"): diagnostics_short_name = postprocessors_short_name.replace( "diagnostics_", "" ->>>>>>> Stashed changes ) if diagnostics_short_name not in _diagnostics_methods: _diagnostics_methods[diagnostics_short_name] = _postprocessors @@ -107,7 +97,7 @@ def discover_postprocessors(): ) else: setattr(ensemblestats, postprocessors_function_name, _postprocessors) - + def postprocessors_info(): """Print all the available postprocessors.""" From e64b70b0e4c0d442ad52dd6ef15b1179aa94139f Mon Sep 17 00:00:00 2001 From: joeycasey87 Date: Mon, 26 Aug 2024 17:11:32 +0200 Subject: [PATCH 10/33] update test_pysteps Changed the test file to match the test updated pysteps test file --- .github/workflows/test_pysteps.yml | 4 ++-- pysteps/postprocessing/interface.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/test_pysteps.yml b/.github/workflows/test_pysteps.yml index fd5642cce..065c28b30 100644 --- a/.github/workflows/test_pysteps.yml +++ b/.github/workflows/test_pysteps.yml @@ -4,11 +4,11 @@ on: # Triggers the workflow on push or pull request events to the master branch push: branches: - - main + - master - pysteps-v2 pull_request: branches: - - main + - master - pysteps-v2 jobs: diff --git a/pysteps/postprocessing/interface.py b/pysteps/postprocessing/interface.py index eef6a95df..dc4b79644 100644 --- a/pysteps/postprocessing/interface.py +++ b/pysteps/postprocessing/interface.py @@ -97,7 +97,7 @@ def discover_postprocessors(): ) else: setattr(ensemblestats, postprocessors_function_name, _postprocessors) - + def postprocessors_info(): """Print all the available postprocessors.""" From 6605643402a58547b501cf2472cb6c4b252198a4 Mon Sep 17 00:00:00 2001 From: Lesley De Cruz Date: Tue, 27 Aug 2024 00:24:29 +0200 Subject: [PATCH 11/33] Remove try-import statements for diagnostics.py These were necessary for the IO plugins, but less so for the diagnostics. --- pysteps/postprocessing/diagnostics.py | 49 --------------------------- 1 file changed, 49 deletions(-) diff --git a/pysteps/postprocessing/diagnostics.py b/pysteps/postprocessing/diagnostics.py index 7f0ad8790..888244d5f 100644 --- a/pysteps/postprocessing/diagnostics.py +++ b/pysteps/postprocessing/diagnostics.py @@ -18,55 +18,6 @@ """ -try: - from osgeo import gdal, gdalconst, osr - - GDAL_IMPORTED = True -except ImportError: - GDAL_IMPORTED = False - -try: - import h5py - - H5PY_IMPORTED = True -except ImportError: - H5PY_IMPORTED = False - -try: - import metranet - - METRANET_IMPORTED = True -except ImportError: - METRANET_IMPORTED = False - -try: - import netCDF4 - - NETCDF4_IMPORTED = True -except ImportError: - NETCDF4_IMPORTED = False - -try: - from PIL import Image - - PIL_IMPORTED = True -except ImportError: - PIL_IMPORTED = False - -try: - import pyproj - - PYPROJ_IMPORTED = True -except ImportError: - PYPROJ_IMPORTED = False - -try: - import pygrib - - PYGRIB_IMPORTED = True -except ImportError: - PYGRIB_IMPORTED = False - def postprocessors_diagnostics_example1(filename, **kwargs): return "Hello, I am an example diagnostics postprocessor." From 2ed3411b1585bc99e3a01353d2fcf8dee1d28bc9 Mon Sep 17 00:00:00 2001 From: Lesley De Cruz Date: Tue, 27 Aug 2024 00:39:08 +0200 Subject: [PATCH 12/33] Revert back to master and Python 3.10 in check_black.yml --- .github/workflows/check_black.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/check_black.yml b/.github/workflows/check_black.yml index c8360540c..41e1ca6ff 100644 --- a/.github/workflows/check_black.yml +++ b/.github/workflows/check_black.yml @@ -11,9 +11,9 @@ name: Check Black on: # Triggers the workflow on push or pull request events but only for the master branch push: - branches: [ main ] + branches: [ master ] pull_request: - branches: [ main ] + branches: [ master ] jobs: build: From 01bf7ba5d1db25b814122abdad08e2abba85cee4 Mon Sep 17 00:00:00 2001 From: Lesley De Cruz Date: Tue, 27 Aug 2024 01:52:05 +0200 Subject: [PATCH 13/33] Make sure the postprocessors are actually discovered in the main init --- pysteps/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/pysteps/__init__.py b/pysteps/__init__.py index 43fa20533..f6126d340 100644 --- a/pysteps/__init__.py +++ b/pysteps/__init__.py @@ -218,3 +218,4 @@ def load_config_file(params_file=None, verbose=False, dryrun=False): # After the sub-modules are loaded, register the discovered importers plugin. io.interface.discover_importers() +postprocessing.interface.discover_postprocessors() From 3586f5614a94a73939009c28672f8f6c47ea2811 Mon Sep 17 00:00:00 2001 From: Lesley De Cruz Date: Tue, 27 Aug 2024 02:03:40 +0200 Subject: [PATCH 14/33] Refactor code for postprocessing plugin detection. Avoid duplicate code, refactor into functions. Also fix a small typo causing a bug: postprocessor_s_ --- pysteps/postprocessing/interface.py | 225 ++++++++++++---------------- 1 file changed, 97 insertions(+), 128 deletions(-) diff --git a/pysteps/postprocessing/interface.py b/pysteps/postprocessing/interface.py index dc4b79644..29e9b7f09 100644 --- a/pysteps/postprocessing/interface.py +++ b/pysteps/postprocessing/interface.py @@ -29,6 +29,33 @@ ) +def add_postprocessor( + postprocessors_short_name, + postprocessors_function_name, + _postprocessors, + methods_dict, + module, +): + short_name = postprocessors_short_name.replace(f"{module}_", "") + if short_name not in methods_dict: + methods_dict[short_name] = _postprocessors + else: + RuntimeWarning( + f"The {module} identifier '{short_name}' is already available in " + f"'pysteps.postprocessing.interface_{module}_methods'.\n" + f"Skipping {entry_point.module_name}:{'.'.join(entry_point.attrs)}" + ) + + if hasattr(globals()[module], postprocessors_short_name): + RuntimeWarning( + f"The {module} function '{short_name}' is already an attribute" + f"of 'pysteps.postprocessing.{module}'.\n" + f"Skipping {entry_point.module_name}:{'.'.join(entry_point.attrs)}" + ) + else: + setattr(globals()[module], postprocessors_function_name, _postprocessors) + + def discover_postprocessors(): """ Search for installed postprocessing plugins in the entrypoint 'pysteps.plugins.postprocessors' @@ -51,159 +78,101 @@ def discover_postprocessors(): postprocessors_function_name = _postprocessors.__name__ postprocessors_short_name = postprocessors_function_name.replace( - "postprocessors_", "" + "postprocessor_", "" ) - if postprocessors_short_name.startswith("diagnostics_"): - diagnostics_short_name = postprocessors_short_name.replace( - "diagnostics_", "" + if "diagnostics" in entry_point.module_name: + add_postprocessor( + postprocessors_short_name, + postprocessors_function_name, + _postprocessors, + _diagnostics_methods, + "diagnostics", ) - if diagnostics_short_name not in _diagnostics_methods: - _diagnostics_methods[diagnostics_short_name] = _postprocessors - else: - RuntimeWarning( - f"The diagnostics identifier '{diagnostics_short_name}' is already available in " - "'pysteps.postprocessing.interface_diagnostics_methods'.\n" - f"Skipping {entry_point.module_name}:{'.'.join(entry_point.attrs)}" - ) - - if hasattr(diagnostics, postprocessors_short_name): - RuntimeWarning( - f"The diagnostics function '{diagnostics_short_name}' is already an attribute" - "of 'pysteps.postprocessing.diagnostics'.\n" - f"Skipping {entry_point.module_name}:{'.'.join(entry_point.attrs)}" - ) - else: - setattr(diagnostics, postprocessors_function_name, _postprocessors) - - elif postprocessors_short_name.startswith("ensemblestats_"): - ensemblestats_short_name = postprocessors_short_name.replace( - "ensemblestats_", "" + elif "ensemblestats" in entry_point.module_name: + add_postprocessor( + postprocessors_short_name, + postprocessors_function_name, + _postprocessors, + _ensemblestats_methods, + "ensemblestats", ) - if ensemblestats_short_name not in _ensemblestats_methods: - _ensemblestats_methods[ensemblestats_short_name] = _postprocessors - else: - RuntimeWarning( - f"The ensemblestats identifier '{ensemblestats_short_name}' is already available in " - "'pysteps.postprocessing.interface_ensemblestats_methods'.\n" - f"Skipping {entry_point.module_name}:{'.'.join(entry_point.attrs)}" - ) - - if hasattr(ensemblestats, postprocessors_short_name): - RuntimeWarning( - f"The ensemblestats function '{ensemblestats_short_name}' is already an attribute" - "of 'pysteps.postprocessing.diagnostics'.\n" - f"Skipping {entry_point.module_name}:{'.'.join(entry_point.attrs)}" - ) - else: - setattr(ensemblestats, postprocessors_function_name, _postprocessors) - - -def postprocessors_info(): - """Print all the available postprocessors.""" - - # diagnostics available in the 'postprocessing.diagnostics' module - available_diagnostics = [ - attr - for attr in dir(pysteps.postprocessing.diagnostics) - if attr.startswith("postprocessors") - ] - - print("\npostprocessors available in the pysteps.postprocessing.diagnostics module") - pprint(available_diagnostics) - - # diagnostics postprocessors declared in the pysteps.postprocessing.get_method interface - diagnostics_in_the_interface = [ - f for f in list(pysteps.postprocessing.interface._diagnostics_methods.keys()) - ] - - print("\ndiagnostics available in the pysteps.postprocessing.get_method interface") - pprint( - [ - (short_name, f.__name__) - for short_name, f in pysteps.postprocessing.interface._diagnostics_methods.items() - ] - ) - - # Let's use sets to find out if there are postprocessors present in the diagnostics module - # but not declared in the interface, and vice versa. - available_diagnostics = set(available_diagnostics) - diagnostics_in_the_interface = set(diagnostics_in_the_interface) - - difference = available_diagnostics ^ diagnostics_in_the_interface - if len(difference) > 0: - print("\nIMPORTANT:") - _diff = available_diagnostics - diagnostics_in_the_interface - if len(_diff) > 0: - print( - "\nIMPORTANT:\nThe following postprocessors are available in pysteps.postprocessing.diagnostics " - "module but not in the pysteps.postprocessing.get_method interface" + else: + raise ValueError( + f"Unknown module {entry_point.module_name} in the entrypoint {entry_point.name}" ) - pprint(_diff) - _diff = diagnostics_in_the_interface - available_diagnostics - if len(_diff) > 0: - print( - "\nWARNING:\n" - "The following postprocessors are available in the pysteps.postprocessing.get_method " - "interface but not in the pysteps.postprocessing.diagnostics module" - ) - pprint(_diff) - # postprocessors available in the 'postprocessing.ensemblestats' module - available_ensemblestats = [ - attr - for attr in dir(pysteps.postprocessing.ensemblestats) - if attr.startswith("postprocessors") - ] - print( - "\npostprocessors available in the pysteps.postprocessing.ensemblestats module" - ) - pprint(available_ensemblestats) +def print_postprocessors_info(module_name, interface_methods, module_methods): + """ + Helper function to print the postprocessors available in the module and in the interface. - # ensemblestats postprocessors declared in the pysteps.postprocessing.get_method interface - ensemblestats_in_the_interface = [ - f for f in list(pysteps.postprocessing.interface._ensemblestats_methods.keys()) - ] + Parameters + ---------- + module_name: str + Name of the module, for example 'pysteps.postprocessing.diagnostics'. + interface_methods: dict + Dictionary of the postprocessors declared in the interface, for example _diagnostics_methods. + module_methods: list + List of the postprocessors available in the module, for example 'postprocessors_diagnostics_example1'. + + """ + print(f"\npostprocessors available in the {module_name} module") + pprint(module_methods) print( - "\npostprocessors available in the pysteps.postprocessing.get_method interface" - ) - pprint( - [ - (short_name, f.__name__) - for short_name, f in pysteps.postprocessing.interface._ensemblestats_methods.items() - ] + f"\npostprocessors available in the pysteps.postprocessing.get_method interface" ) + pprint([(short_name, f.__name__) for short_name, f in interface_methods.items()]) - # Let's use sets to find out if there are postprocessors present in the ensemblestats module - # but not declared in the interface, and vice versa. - available_ensemblestats = set(available_ensemblestats) - ensemblestats_in_the_interface = set(ensemblestats_in_the_interface) + module_methods_set = set(module_methods) + interface_methods_set = set(interface_methods.keys()) - difference = available_ensemblestats ^ ensemblestats_in_the_interface + difference = module_methods_set ^ interface_methods_set if len(difference) > 0: print("\nIMPORTANT:") - _diff = available_ensemblestats - ensemblestats_in_the_interface + _diff = module_methods_set - interface_methods_set if len(_diff) > 0: print( - "\nIMPORTANT:\nThe following postprocessors are available in pysteps.postprocessing.ensemblestats " - "module but not in the pysteps.postprocessing.get_method interface" + f"\nIMPORTANT:\nThe following postprocessors are available in {module_name} module but not in the pysteps.postprocessing.get_method interface" ) pprint(_diff) - _diff = ensemblestats_in_the_interface - available_ensemblestats + _diff = interface_methods_set - module_methods_set if len(_diff) > 0: print( "\nWARNING:\n" - "The following postprocessors are available in the pysteps.postprocessing.get_method " - "interface but not in the pysteps.postprocessing.ensemblestats module" + f"The following postprocessors are available in the pysteps.postprocessing.get_method interface but not in the {module_name} module" ) pprint(_diff) - available_postprocessors = available_diagnostics.union(available_ensemblestats) - postprocessors_in_the_interface = ensemblestats_in_the_interface.union( - diagnostics_in_the_interface - ) + +def postprocessors_info(): + """Print all the available postprocessors.""" + + available_postprocessors = set() + postprocessors_in_the_interface = set() + # Discover the postprocessors available in the plugins + for plugintype in ["diagnostics", "ensemblestats"]: + interface_methods = ( + _diagnostics_methods + if plugintype == "diagnostics" + else _ensemblestats_methods + ) + module_name = f"pysteps.postprocessing.{plugintype}" + available_module_methods = [ + attr + for attr in dir(importlib.import_module(module_name)) + if attr.startswith("postprocessors") + ] + print_postprocessors_info( + module_name, interface_methods, available_module_methods + ) + available_postprocessors = available_postprocessors.union( + available_module_methods + ) + postprocessors_in_the_interface = postprocessors_in_the_interface.union( + interface_methods.keys() + ) return available_postprocessors, postprocessors_in_the_interface From f581846cee2fe1d682c9d2266abf971e92945e68 Mon Sep 17 00:00:00 2001 From: Lesley De Cruz Date: Tue, 28 Jan 2025 17:56:06 +0100 Subject: [PATCH 15/33] Update dummy code names for new plugin structure --- pysteps/postprocessing/diagnostics.py | 4 ++-- pysteps/postprocessing/ensemblestats.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/pysteps/postprocessing/diagnostics.py b/pysteps/postprocessing/diagnostics.py index 888244d5f..2093f43dc 100644 --- a/pysteps/postprocessing/diagnostics.py +++ b/pysteps/postprocessing/diagnostics.py @@ -19,9 +19,9 @@ """ -def postprocessors_diagnostics_example1(filename, **kwargs): +def diagnostics_example1(filename, **kwargs): return "Hello, I am an example diagnostics postprocessor." -def postprocessors_diagnostics_example2(filename, **kwargs): +def diagnostics_example2(filename, **kwargs): return [[42, 42], [42, 42]] diff --git a/pysteps/postprocessing/ensemblestats.py b/pysteps/postprocessing/ensemblestats.py index 21323f9bd..2093bf224 100644 --- a/pysteps/postprocessing/ensemblestats.py +++ b/pysteps/postprocessing/ensemblestats.py @@ -179,9 +179,9 @@ def banddepth(X, thr=None, norm=False): return depth -def postprocessors_ensemblestats_example1(filename, **kwargs): +def ensemblestats_example1(filename, **kwargs): return "Hello, I am an example of postprocessing ensemble statistics." -def postprocessors_ensemblestats_example2(filename, **kwargs): +def ensemblestats_example2(filename, **kwargs): return [[42, 42], [42, 42]] From cb89616ee72afa28be435677d49aacd5ed93b2cc Mon Sep 17 00:00:00 2001 From: Lesley De Cruz Date: Tue, 28 Jan 2025 17:56:29 +0100 Subject: [PATCH 16/33] Change postprocessor interface to use diagnostic_ and ensemblestat_ plugins --- pysteps/postprocessing/interface.py | 38 +++++++++++++++++------------ 1 file changed, 22 insertions(+), 16 deletions(-) diff --git a/pysteps/postprocessing/interface.py b/pysteps/postprocessing/interface.py index 29e9b7f09..6629ceef5 100644 --- a/pysteps/postprocessing/interface.py +++ b/pysteps/postprocessing/interface.py @@ -18,25 +18,35 @@ from pysteps.postprocessing import diagnostics, ensemblestats from pprint import pprint -_diagnostics_methods = dict( - diagnostics_example1=diagnostics.postprocessors_diagnostics_example1, - diagnostics_example3=lambda x: [x, x], +_diagnostic_methods = dict( ) -_ensemblestats_methods = dict( - ensemblestats_example1=ensemblestats.postprocessors_ensemblestats_example1, - ensemblestats_example3=lambda x, y: [x, y], +_ensemblestat_methods = dict( + mean=ensemblestats.mean, + excprob=ensemblestats.excprob, + banddepth=ensemblestats.banddepth, ) - def add_postprocessor( - postprocessors_short_name, + """ + Add the postprocessor to the appropriate _methods dictionary and to the module. + Parameters + ---------- + + postprocessors_function_name: str + for example, e.g. diagnostic_example1 + _postprocessors: function + the function to be added + @param methods_dict: the dictionary where the function is added + @param module: the module where the function is added, e.g. 'diagnostics' + """ postprocessors_function_name, _postprocessors, methods_dict, module, ): - short_name = postprocessors_short_name.replace(f"{module}_", "") + + short_name = postprocessors_function_name.replace(f"{module}_", "") if short_name not in methods_dict: methods_dict[short_name] = _postprocessors else: @@ -77,25 +87,21 @@ def discover_postprocessors(): _postprocessors = entry_point.load() postprocessors_function_name = _postprocessors.__name__ - postprocessors_short_name = postprocessors_function_name.replace( - "postprocessor_", "" - ) + if "diagnostics" in entry_point.module_name: add_postprocessor( - postprocessors_short_name, postprocessors_function_name, _postprocessors, _diagnostics_methods, - "diagnostics", + "diagnostic", ) elif "ensemblestats" in entry_point.module_name: add_postprocessor( - postprocessors_short_name, postprocessors_function_name, _postprocessors, _ensemblestats_methods, - "ensemblestats", + "ensemblestat", ) else: raise ValueError( From f9bf9cf5063d17ffb83441ed27816d7e1fb24fdb Mon Sep 17 00:00:00 2001 From: Felix Erdmann Date: Wed, 29 Jan 2025 13:29:48 +0100 Subject: [PATCH 17/33] Fix the postprocessing interface - should match names as in the plugin cookiecutter --- pysteps/postprocessing/interface.py | 37 +++++++++++++++++------------ 1 file changed, 22 insertions(+), 15 deletions(-) diff --git a/pysteps/postprocessing/interface.py b/pysteps/postprocessing/interface.py index 6629ceef5..0a51182fb 100644 --- a/pysteps/postprocessing/interface.py +++ b/pysteps/postprocessing/interface.py @@ -18,16 +18,22 @@ from pysteps.postprocessing import diagnostics, ensemblestats from pprint import pprint -_diagnostic_methods = dict( +_diagnostics_methods = dict( ) -_ensemblestat_methods = dict( +_ensemblestats_methods = dict( mean=ensemblestats.mean, excprob=ensemblestats.excprob, banddepth=ensemblestats.banddepth, ) def add_postprocessor( + postprocessors_function_name, + _postprocessors, + methods_dict, + module, + attributes +): """ Add the postprocessor to the appropriate _methods dictionary and to the module. Parameters @@ -39,28 +45,27 @@ def add_postprocessor( the function to be added @param methods_dict: the dictionary where the function is added @param module: the module where the function is added, e.g. 'diagnostics' + @param attributes: the existing functions in the selected module """ - postprocessors_function_name, - _postprocessors, - methods_dict, - module, -): - short_name = postprocessors_function_name.replace(f"{module}_", "") + # module_name ends with an "s", the function prefix is without "s" + function_prefix = module[:-1] + # get funtion name without mo + short_name = postprocessors_function_name.replace(f"{function_prefix}_", "") if short_name not in methods_dict: methods_dict[short_name] = _postprocessors else: RuntimeWarning( f"The {module} identifier '{short_name}' is already available in " f"'pysteps.postprocessing.interface_{module}_methods'.\n" - f"Skipping {entry_point.module_name}:{'.'.join(entry_point.attrs)}" + f"Skipping {module}:{'.'.join(attributes)}" ) - if hasattr(globals()[module], postprocessors_short_name): + if hasattr(globals()[module], postprocessors_function_name): RuntimeWarning( f"The {module} function '{short_name}' is already an attribute" f"of 'pysteps.postprocessing.{module}'.\n" - f"Skipping {entry_point.module_name}:{'.'.join(entry_point.attrs)}" + f"Skipping {module}:{'.'.join(attributes)}" ) else: setattr(globals()[module], postprocessors_function_name, _postprocessors) @@ -89,19 +94,21 @@ def discover_postprocessors(): postprocessors_function_name = _postprocessors.__name__ - if "diagnostics" in entry_point.module_name: + if "diagnostic" in entry_point.module_name: add_postprocessor( postprocessors_function_name, _postprocessors, _diagnostics_methods, - "diagnostic", + "diagnostics", + entry_point.attrs, ) - elif "ensemblestats" in entry_point.module_name: + elif "ensemblestat" in entry_point.module_name: add_postprocessor( postprocessors_function_name, _postprocessors, _ensemblestats_methods, - "ensemblestat", + "ensemblestats", + entry_point.attrs, ) else: raise ValueError( From 13922fc47c1171a5908eff36f0cb2a4d286f9794 Mon Sep 17 00:00:00 2001 From: Felix Erdmann Date: Wed, 29 Jan 2025 18:30:57 +0100 Subject: [PATCH 18/33] Update postprocessing package to work with plugins - diagnostic plugins created with the cookiecutter are now correctly recognized and implemented --- pysteps/postprocessing/diagnostics.py | 6 +-- pysteps/postprocessing/ensemblestats.py | 4 +- pysteps/postprocessing/interface.py | 68 +++++++++++++------------ 3 files changed, 40 insertions(+), 38 deletions(-) diff --git a/pysteps/postprocessing/diagnostics.py b/pysteps/postprocessing/diagnostics.py index 2093f43dc..ac7802e1a 100644 --- a/pysteps/postprocessing/diagnostics.py +++ b/pysteps/postprocessing/diagnostics.py @@ -6,7 +6,7 @@ The methods in this module implement the following interface:: - diagnostics_xxx(optional arguments) + diagnostic_xxx(optional arguments) where **xxx** is the name of the diagnostic to be applied. @@ -19,9 +19,9 @@ """ -def diagnostics_example1(filename, **kwargs): +def diagnostic_example1(filename, **kwargs): return "Hello, I am an example diagnostics postprocessor." -def diagnostics_example2(filename, **kwargs): +def diagnostic_example2(filename, **kwargs): return [[42, 42], [42, 42]] diff --git a/pysteps/postprocessing/ensemblestats.py b/pysteps/postprocessing/ensemblestats.py index 2093bf224..8249ac66d 100644 --- a/pysteps/postprocessing/ensemblestats.py +++ b/pysteps/postprocessing/ensemblestats.py @@ -179,9 +179,9 @@ def banddepth(X, thr=None, norm=False): return depth -def ensemblestats_example1(filename, **kwargs): +def ensemblestat_example1(filename, **kwargs): return "Hello, I am an example of postprocessing ensemble statistics." -def ensemblestats_example2(filename, **kwargs): +def ensemblestat_example2(filename, **kwargs): return [[42, 42], [42, 42]] diff --git a/pysteps/postprocessing/interface.py b/pysteps/postprocessing/interface.py index 0a51182fb..6be8139da 100644 --- a/pysteps/postprocessing/interface.py +++ b/pysteps/postprocessing/interface.py @@ -48,10 +48,8 @@ def add_postprocessor( @param attributes: the existing functions in the selected module """ - # module_name ends with an "s", the function prefix is without "s" - function_prefix = module[:-1] # get funtion name without mo - short_name = postprocessors_function_name.replace(f"{function_prefix}_", "") + short_name = postprocessors_function_name.replace(f"{module}_", "") if short_name not in methods_dict: methods_dict[short_name] = _postprocessors else: @@ -86,34 +84,36 @@ def discover_postprocessors(): importlib.reload(pkg_resources) - for entry_point in pkg_resources.iter_entry_points( - group="pysteps.plugins.postprocessors", name=None - ): - _postprocessors = entry_point.load() - - postprocessors_function_name = _postprocessors.__name__ - - - if "diagnostic" in entry_point.module_name: - add_postprocessor( - postprocessors_function_name, - _postprocessors, - _diagnostics_methods, - "diagnostics", - entry_point.attrs, - ) - elif "ensemblestat" in entry_point.module_name: - add_postprocessor( - postprocessors_function_name, - _postprocessors, - _ensemblestats_methods, - "ensemblestats", - entry_point.attrs, - ) - else: - raise ValueError( - f"Unknown module {entry_point.module_name} in the entrypoint {entry_point.name}" - ) + # Discover the postprocessors available in the plugins + for plugintype in ["diagnostic", "ensemblestat"]: + for entry_point in pkg_resources.iter_entry_points( + group=f"pysteps.plugins.{plugintype}", name=None + ): + _postprocessors = entry_point.load() + + postprocessors_function_name = _postprocessors.__name__ + + + if "diagnostic" in entry_point.module_name: + add_postprocessor( + postprocessors_function_name, + _postprocessors, + _diagnostics_methods, + "diagnostics", + entry_point.attrs, + ) + elif "ensemblestat" in entry_point.module_name: + add_postprocessor( + postprocessors_function_name, + _postprocessors, + _ensemblestats_methods, + "ensemblestats", + entry_point.attrs, + ) + else: + raise ValueError( + f"Unknown module {entry_point.module_name} in the entrypoint {entry_point.name}" + ) def print_postprocessors_info(module_name, interface_methods, module_methods): @@ -164,7 +164,7 @@ def postprocessors_info(): available_postprocessors = set() postprocessors_in_the_interface = set() - # Discover the postprocessors available in the plugins + # List the plugins that have been added to the postprocessing.[plugintype] module for plugintype in ["diagnostics", "ensemblestats"]: interface_methods = ( _diagnostics_methods @@ -175,8 +175,10 @@ def postprocessors_info(): available_module_methods = [ attr for attr in dir(importlib.import_module(module_name)) - if attr.startswith("postprocessors") + if attr.startswith(plugintype[:-1]) ] + # add the pre-existing ensemblestats functions (see _ensemblestats_methods above) + if "ensemblestats" in plugintype: available_module_methods += ["mean","excprob","banddepth"] print_postprocessors_info( module_name, interface_methods, available_module_methods ) From cb2529cbed532c076becb83aaf92938b7542da4b Mon Sep 17 00:00:00 2001 From: Felix Erdmann Date: Thu, 30 Jan 2025 15:49:35 +0100 Subject: [PATCH 19/33] Update both io interface and postprocessign interface - importer and diagnostic plugins correctly recognized in entry points - cleaning: removed unused import modules --- pysteps/io/interface.py | 15 ++++++--------- pysteps/postprocessing/interface.py | 19 +++++++++++-------- 2 files changed, 17 insertions(+), 17 deletions(-) diff --git a/pysteps/io/interface.py b/pysteps/io/interface.py index 7275875e5..6fda0935f 100644 --- a/pysteps/io/interface.py +++ b/pysteps/io/interface.py @@ -14,11 +14,8 @@ """ import importlib -from pkg_resources import iter_entry_points - -from pysteps import io from pysteps.decorators import postprocess_import -from pysteps.io import importers, exporters +from pysteps.io import importers, exporters, interface from pprint import pprint _importer_methods = dict( @@ -58,7 +55,7 @@ def discover_importers(): importlib.reload(pkg_resources) for entry_point in pkg_resources.iter_entry_points( - group="pysteps.plugins.importers", name=None + group="pysteps.plugins.importer", name=None ): _importer = entry_point.load() @@ -91,7 +88,7 @@ def importers_info(): # Importers available in the `io.importers` module available_importers = [ - attr for attr in dir(io.importers) if attr.startswith("import_") + attr for attr in dir(importers) if attr.startswith("import_") ] print("\nImporters available in the pysteps.io.importers module") @@ -99,14 +96,14 @@ def importers_info(): # Importers declared in the pysteps.io.get_method interface importers_in_the_interface = [ - f.__name__ for f in io.interface._importer_methods.values() + f.__name__ for f in interface._importer_methods.values() ] print("\nImporters available in the pysteps.io.get_method interface") pprint( [ (short_name, f.__name__) - for short_name, f in io.interface._importer_methods.items() + for short_name, f in interface._importer_methods.items() ] ) @@ -117,7 +114,7 @@ def importers_info(): difference = available_importers ^ importers_in_the_interface if len(difference) > 0: - print("\nIMPORTANT:") + #print("\nIMPORTANT:") _diff = available_importers - importers_in_the_interface if len(_diff) > 0: print( diff --git a/pysteps/postprocessing/interface.py b/pysteps/postprocessing/interface.py index 6be8139da..1baf6644f 100644 --- a/pysteps/postprocessing/interface.py +++ b/pysteps/postprocessing/interface.py @@ -14,7 +14,6 @@ """ import importlib -import pysteps.postprocessing from pysteps.postprocessing import diagnostics, ensemblestats from pprint import pprint @@ -127,7 +126,7 @@ def print_postprocessors_info(module_name, interface_methods, module_methods): interface_methods: dict Dictionary of the postprocessors declared in the interface, for example _diagnostics_methods. module_methods: list - List of the postprocessors available in the module, for example 'postprocessors_diagnostics_example1'. + List of the postprocessors available in the module, for example 'diagnostic_example1'. """ print(f"\npostprocessors available in the {module_name} module") @@ -143,7 +142,7 @@ def print_postprocessors_info(module_name, interface_methods, module_methods): difference = module_methods_set ^ interface_methods_set if len(difference) > 0: - print("\nIMPORTANT:") + #print("\nIMPORTANT:") _diff = module_methods_set - interface_methods_set if len(_diff) > 0: print( @@ -166,11 +165,13 @@ def postprocessors_info(): postprocessors_in_the_interface = set() # List the plugins that have been added to the postprocessing.[plugintype] module for plugintype in ["diagnostics", "ensemblestats"]: + # in the dictionary and found by get_methods() function interface_methods = ( _diagnostics_methods if plugintype == "diagnostics" else _ensemblestats_methods ) + # in the pysteps.postprocessing module module_name = f"pysteps.postprocessing.{plugintype}" available_module_methods = [ attr @@ -178,7 +179,9 @@ def postprocessors_info(): if attr.startswith(plugintype[:-1]) ] # add the pre-existing ensemblestats functions (see _ensemblestats_methods above) - if "ensemblestats" in plugintype: available_module_methods += ["mean","excprob","banddepth"] + # that do not follow the convention to start with "ensemblestat_" as the plugins + if "ensemblestats" in plugintype: + available_module_methods += [em for em in _ensemblestats_methods.keys() if not em.startswith('ensemblestat_')] print_postprocessors_info( module_name, interface_methods, available_module_methods ) @@ -209,10 +212,10 @@ def get_method(name, method_type): +---------------+-------------------------------------------------------+ | Name | Description | +===============+=======================================================+ - | Diagnostics | Example that returns a string | + | Diagnostic | Example that returns a string | | Example1 | | +---------------+-------------------------------------------------------+ - | Diagnostics | Example that returns an array | + | Diagnostic | Example that returns an array | | Example3 | | +---------------+-------------------------------------------------------+ @@ -223,10 +226,10 @@ def get_method(name, method_type): +---------------+-------------------------------------------------------+ | Name | Description | +===============+=======================================================+ - | EnsembleStats | Example that returns a string | + | EnsembleStat | Example that returns a string | | Example1 | | +---------------+-------------------------------------------------------+ - | EnsembleStats | Example that returns an array | + | EnsembleStat | Example that returns an array | | Example3 | | +---------------+-------------------------------------------------------+ From 198df511307fa4cfb196898046539ff3d9ffac38 Mon Sep 17 00:00:00 2001 From: Felix Erdmann Date: Tue, 25 Feb 2025 12:26:05 +0100 Subject: [PATCH 20/33] Reformatted files with pre-commit --- pysteps/io/interface.py | 2 +- pysteps/postprocessing/diagnostics.py | 9 ++++----- pysteps/postprocessing/ensemblestats.py | 8 ++++---- pysteps/postprocessing/interface.py | 23 +++++++++++------------ 4 files changed, 20 insertions(+), 22 deletions(-) diff --git a/pysteps/io/interface.py b/pysteps/io/interface.py index 6fda0935f..aa73eb034 100644 --- a/pysteps/io/interface.py +++ b/pysteps/io/interface.py @@ -114,7 +114,7 @@ def importers_info(): difference = available_importers ^ importers_in_the_interface if len(difference) > 0: - #print("\nIMPORTANT:") + # print("\nIMPORTANT:") _diff = available_importers - importers_in_the_interface if len(_diff) > 0: print( diff --git a/pysteps/postprocessing/diagnostics.py b/pysteps/postprocessing/diagnostics.py index ac7802e1a..333fb97ac 100644 --- a/pysteps/postprocessing/diagnostics.py +++ b/pysteps/postprocessing/diagnostics.py @@ -18,10 +18,9 @@ """ +# def diagnostic_example1(filename, **kwargs): +# return "Hello, I am an example diagnostics postprocessor." -def diagnostic_example1(filename, **kwargs): - return "Hello, I am an example diagnostics postprocessor." - -def diagnostic_example2(filename, **kwargs): - return [[42, 42], [42, 42]] +# def diagnostic_example2(filename, **kwargs): +# return [[42, 42], [42, 42]] diff --git a/pysteps/postprocessing/ensemblestats.py b/pysteps/postprocessing/ensemblestats.py index 8249ac66d..1306a4325 100644 --- a/pysteps/postprocessing/ensemblestats.py +++ b/pysteps/postprocessing/ensemblestats.py @@ -179,9 +179,9 @@ def banddepth(X, thr=None, norm=False): return depth -def ensemblestat_example1(filename, **kwargs): - return "Hello, I am an example of postprocessing ensemble statistics." +# def ensemblestat_example1(filename, **kwargs): +# return "Hello, I am an example of postprocessing ensemble statistics." -def ensemblestat_example2(filename, **kwargs): - return [[42, 42], [42, 42]] +# def ensemblestat_example2(filename, **kwargs): +# return [[42, 42], [42, 42]] diff --git a/pysteps/postprocessing/interface.py b/pysteps/postprocessing/interface.py index 1baf6644f..f71a1fb76 100644 --- a/pysteps/postprocessing/interface.py +++ b/pysteps/postprocessing/interface.py @@ -17,8 +17,7 @@ from pysteps.postprocessing import diagnostics, ensemblestats from pprint import pprint -_diagnostics_methods = dict( -) +_diagnostics_methods = dict() _ensemblestats_methods = dict( mean=ensemblestats.mean, @@ -26,12 +25,9 @@ banddepth=ensemblestats.banddepth, ) + def add_postprocessor( - postprocessors_function_name, - _postprocessors, - methods_dict, - module, - attributes + postprocessors_function_name, _postprocessors, methods_dict, module, attributes ): """ Add the postprocessor to the appropriate _methods dictionary and to the module. @@ -89,10 +85,9 @@ def discover_postprocessors(): group=f"pysteps.plugins.{plugintype}", name=None ): _postprocessors = entry_point.load() - + postprocessors_function_name = _postprocessors.__name__ - - + if "diagnostic" in entry_point.module_name: add_postprocessor( postprocessors_function_name, @@ -142,7 +137,7 @@ def print_postprocessors_info(module_name, interface_methods, module_methods): difference = module_methods_set ^ interface_methods_set if len(difference) > 0: - #print("\nIMPORTANT:") + # print("\nIMPORTANT:") _diff = module_methods_set - interface_methods_set if len(_diff) > 0: print( @@ -181,7 +176,11 @@ def postprocessors_info(): # add the pre-existing ensemblestats functions (see _ensemblestats_methods above) # that do not follow the convention to start with "ensemblestat_" as the plugins if "ensemblestats" in plugintype: - available_module_methods += [em for em in _ensemblestats_methods.keys() if not em.startswith('ensemblestat_')] + available_module_methods += [ + em + for em in _ensemblestats_methods.keys() + if not em.startswith("ensemblestat_") + ] print_postprocessors_info( module_name, interface_methods, available_module_methods ) From 1305e8856dc0c0c313892c5df8b460f86f201a52 Mon Sep 17 00:00:00 2001 From: Lesley De Cruz Date: Tue, 25 Feb 2025 14:40:55 +0100 Subject: [PATCH 21/33] Remove tests for diagnostics plugins interfaces. --- pysteps/tests/test_interfaces.py | 76 -------------------------------- 1 file changed, 76 deletions(-) diff --git a/pysteps/tests/test_interfaces.py b/pysteps/tests/test_interfaces.py index f652816a7..c490b2539 100644 --- a/pysteps/tests/test_interfaces.py +++ b/pysteps/tests/test_interfaces.py @@ -152,82 +152,6 @@ def method_getter(name): pysteps.io.interface.get_method("mch_gif", "io") -def test_postprocessing_interface(): - """Test the postprocessing module interface.""" - - import pysteps.postprocessing as postprocessing - from pysteps.postprocessing import postprocessors_diagnostics_example1 - from pysteps.postprocessing import postprocessors_diagnostics_example2 - from pysteps.postprocessing import postprocessors_ensemblestats_example1 - from pysteps.postprocessing import postprocessors_ensemblestats_example2 - - valid_diagnostics_names_func_pair = [ - ("diagnostics_example1", postprocessors_diagnostics_example1) - ] - - def method_getter_diagnostics(name): - return pysteps.postprocessing.interface.get_method(name, "diagnostics") - - invalid_names = ["bom", "fmi", "knmi", "mch", "mrms", "opera", "saf"] - _generic_interface_test( - method_getter_diagnostics, valid_diagnostics_names_func_pair, invalid_names - ) - - valid_ensemblestats_names_func_pair = [ - ("ensemblestats_example1", postprocessors_ensemblestats_example1) - ] - - def method_getter_ensemblestats(name): - return pysteps.postprocessing.interface.get_method(name, "ensemblestats") - - invalid_names = ["bom", "fmi", "knmi", "mch", "mrms", "opera", "saf"] - _generic_interface_test( - method_getter_ensemblestats, valid_ensemblestats_names_func_pair, invalid_names - ) - - # Test for invalid argument type - with pytest.raises(TypeError): - pysteps.postprocessing.interface.get_method("diagnostics_example1", None) - pysteps.postprocessing.interface.get_method(None, "diagnostics") - - # Test for invalid method types - with pytest.raises(ValueError): - pysteps.postprocessing.interface.get_method("diagnostics_example1", "io") - - with pytest.raises(TypeError): - pysteps.postprocessing.interface.get_method(24, "diagnostics") - - assert isinstance( - pysteps.postprocessing.interface.postprocessors_info()[0], - set, - ) - - assert isinstance( - pysteps.postprocessing.interface.postprocessors_info()[1], - set, - ) - - assert isinstance( - postprocessors_diagnostics_example1(filename="example_filename"), - str, - ) - - assert isinstance( - postprocessors_diagnostics_example2(filename="example_filename"), - list, - ) - - assert isinstance( - postprocessors_ensemblestats_example1(filename="example_filename"), - str, - ) - - assert isinstance( - postprocessors_ensemblestats_example2(filename="example_filename"), - list, - ) - - def test_motion_interface(): """Test the motion module interface.""" From 296694de39986a9e58048d9b6043ecfacdca95d7 Mon Sep 17 00:00:00 2001 From: Felix Erdmann Date: Wed, 26 Feb 2025 11:56:12 +0100 Subject: [PATCH 22/33] Fix io.interface to work with the new cookiecutter --- pysteps/io/interface.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pysteps/io/interface.py b/pysteps/io/interface.py index 5015f6280..c9ae754f0 100644 --- a/pysteps/io/interface.py +++ b/pysteps/io/interface.py @@ -69,14 +69,14 @@ def discover_importers(): RuntimeWarning( f"The importer identifier '{importer_short_name}' is already available in" "'pysteps.io.interface._importer_methods'.\n" - f"Skipping {entry_point.module}:{entry_point.attr}" + f"Skipping {entry_point.module_name}:{entry_point.attrs}" ) if hasattr(importers, importer_function_name): RuntimeWarning( f"The importer function '{importer_function_name}' is already an attribute" "of 'pysteps.io.importers`.\n" - f"Skipping {entry_point.module}:{entry_point.attr}" + f"Skipping {entry_point.module_name}:{entry_point.attrs}" ) else: setattr(importers, importer_function_name, _importer) From a20ce17022dc95c0e78d588fd0f4124782e07670 Mon Sep 17 00:00:00 2001 From: Lesley De Cruz Date: Thu, 27 Feb 2025 00:13:58 +0100 Subject: [PATCH 23/33] Test if the default cookiecutter plugin can be loaded in the CI tests. --- ci/test_plugin_support.py | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/ci/test_plugin_support.py b/ci/test_plugin_support.py index 3f8984247..3b43cb3c4 100644 --- a/ci/test_plugin_support.py +++ b/ci/test_plugin_support.py @@ -13,15 +13,11 @@ from pysteps import io print("Testing plugin support: ", end="") -assert hasattr(io.importers, "import_abc_xxx") -assert hasattr(io.importers, "import_abc_yyy") +assert hasattr(io.importers, "import_institution_name") -assert "abc_xxx" in io.interface._importer_methods -assert "abc_yyy" in io.interface._importer_methods +assert "institution_name" in io.interface._importer_methods -from pysteps.io.importers import import_abc_xxx, import_abc_yyy - -import_abc_xxx("filename") -import_abc_yyy("filename") +from pysteps.io.importers import import_institution_name +import_institution_name("filename") print("PASSED") From a4e0b6ab47eb2851187ad69974c5b21e39576217 Mon Sep 17 00:00:00 2001 From: Lesley De Cruz Date: Thu, 27 Feb 2025 00:15:42 +0100 Subject: [PATCH 24/33] Fix default plugin path in tox.ini. --- tox.ini | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/tox.ini b/tox.ini index f37444840..013e91354 100644 --- a/tox.ini +++ b/tox.ini @@ -65,9 +65,8 @@ commands = # Test the pysteps plugin support pip install cookiecutter cookiecutter -f --no-input https://github.com/pySTEPS/cookiecutter-pysteps-plugin -o {temp_dir}/ - pip install {temp_dir}/pysteps-importer-abc + pip install {temp_dir}/pysteps-importer-institution-name python {toxinidir}/ci/test_plugin_support.py - # Check the compiled modules python -c "from pysteps import motion" python -c "from pysteps.motion import vet" From f340085fc15d8ddd89abc402f299d92a7092dd60 Mon Sep 17 00:00:00 2001 From: Lesley De Cruz Date: Thu, 27 Feb 2025 00:16:42 +0100 Subject: [PATCH 25/33] Fix plugin tests to use new cookiecutter template; try out importer and diagnostic plugin. --- pysteps/tests/test_plugins_support.py | 43 +++++++++++++++++---------- 1 file changed, 28 insertions(+), 15 deletions(-) diff --git a/pysteps/tests/test_plugins_support.py b/pysteps/tests/test_plugins_support.py index bbf3bcf5b..280bc5c75 100644 --- a/pysteps/tests/test_plugins_support.py +++ b/pysteps/tests/test_plugins_support.py @@ -17,29 +17,41 @@ PLUGIN_TEMPLATE_URL = "https://github.com/pysteps/cookiecutter-pysteps-plugin" from contextlib import contextmanager -import pysteps +from pysteps import io, postprocessing -def _check_installed_plugin(import_func_name): +def _check_installed_importer_plugin(import_func_name): # reload the pysteps module to detect the installed plugin - pysteps.io.discover_importers() - assert hasattr(pysteps.io.importers, import_func_name) - assert import_func_name in pysteps.io.interface._importer_methods - importer = getattr(pysteps.io.importers, import_func_name) + io.discover_importers() + print(io.importers_info()) + import_func_name = import_func_name.replace("importer_", "import_") + assert hasattr(io.importers, import_func_name) + func_name = import_func_name.replace("import_", "") + assert func_name in io.interface._importer_methods + importer = getattr(io.importers, import_func_name) importer("filename") +def _check_installed_diagnostic_plugin(diagnostic_func_name): + # reload the pysteps module to detect the installed plugin + postprocessing.discover_postprocessors() + assert hasattr(postprocessing.diagnostics, diagnostic_func_name) + assert diagnostic_func_name in postprocessing.interface._diagnostics_methods + diagnostic = getattr(postprocessing.diagnostics, diagnostic_func_name) + diagnostic("filename") + + @contextmanager -def _create_and_install_plugin(project_name, importer_name): +def _create_and_install_plugin(project_name, plugin_type): with tempfile.TemporaryDirectory() as tmpdirname: - print(f"Installing plugin {project_name} providing the {importer_name} module") + print(f"Installing plugin {project_name} providing a {plugin_type} module") cookiecutter( PLUGIN_TEMPLATE_URL, no_input=True, overwrite_if_exists=True, extra_context={ "project_name": project_name, - "importer_name": importer_name, + "plugin_type": plugin_type, }, output_dir=tmpdirname, ) @@ -73,9 +85,10 @@ def _uninstall_plugin(project_name): def test_importers_plugins(): - with _create_and_install_plugin("test_importer_aaa", "importer_aaa"): - # The default plugin template appends an _xxx to the importer function. - _check_installed_plugin("importer_aaa_xxx") - with _create_and_install_plugin("test_importer_bbb", "importer_bbb"): - _check_installed_plugin("importer_aaa_xxx") - _check_installed_plugin("importer_bbb_xxx") + with _create_and_install_plugin("pysteps-importer-institution-fun", "importer"): + _check_installed_importer_plugin("importer_institution_fun") + + +def test_diagnostic_plugins(): + with _create_and_install_plugin("pysteps-diagnostic-fun", "diagnostic"): + _check_installed_diagnostic_plugin("diagnostic_fun") From 06d181ac52f16f21061ce8385e191b16e2d04418 Mon Sep 17 00:00:00 2001 From: Felix Erdmann Date: Thu, 27 Feb 2025 13:00:08 +0100 Subject: [PATCH 26/33] Added postprocessing module interface test --- pysteps/tests/test_interfaces.py | 39 ++++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/pysteps/tests/test_interfaces.py b/pysteps/tests/test_interfaces.py index c490b2539..0862b412f 100644 --- a/pysteps/tests/test_interfaces.py +++ b/pysteps/tests/test_interfaces.py @@ -150,6 +150,45 @@ def method_getter(name): # Test for invalid method types with pytest.raises(ValueError): pysteps.io.interface.get_method("mch_gif", "io") + + +def test_postprocessing_interface(): + """Test the postprocessing module interface.""" + + # ensemblestats pre-installed methods + from pysteps.postprocessing import mean, excprob, banddepth + + # Test ensemblestats + valid_names_func_pair = [ + ("mean", mean), + ("excprob", excprob), + ("banddepth", banddepth), + ] + + def method_getter(name): + return pysteps.postprocessing.interface.get_method(name, "ensemblestat") + + invalid_names = ["ensemblestat_mean", "ensemblestat_excprob", "ensemblestat_banddepth"] + _generic_interface_test(method_getter, valid_names_func_pair, invalid_names) + + # Test diagnostics + def method_getter(name): + return pysteps.io.interface.get_method(name, "diagnostic") + + valid_names_func_pair = [] + invalid_names = ["unknown"] + + _generic_interface_test(method_getter, valid_names_func_pair, invalid_names) + + # Test for invalid argument type + with pytest.raises(TypeError): + pysteps.postprocessing.interface.get_method("mean", None) + pysteps.postprocessing.interface.get_method(None, "ensemblestat") + pysteps.postprocessing.interface.get_method(None, "diagnostic") + + # Test for invalid method types + with pytest.raises(ValueError): + pysteps.postprocessing.interface.get_method("mean", "forecast") def test_motion_interface(): From 17397e537732d39d6820735f322a606c2017f04f Mon Sep 17 00:00:00 2001 From: Felix Erdmann Date: Thu, 27 Feb 2025 13:07:43 +0100 Subject: [PATCH 27/33] Postprocessing interface reformatted with pre-commit --- pysteps/tests/test_interfaces.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/pysteps/tests/test_interfaces.py b/pysteps/tests/test_interfaces.py index 0862b412f..ae4e80b92 100644 --- a/pysteps/tests/test_interfaces.py +++ b/pysteps/tests/test_interfaces.py @@ -150,11 +150,11 @@ def method_getter(name): # Test for invalid method types with pytest.raises(ValueError): pysteps.io.interface.get_method("mch_gif", "io") - + def test_postprocessing_interface(): """Test the postprocessing module interface.""" - + # ensemblestats pre-installed methods from pysteps.postprocessing import mean, excprob, banddepth @@ -168,7 +168,11 @@ def test_postprocessing_interface(): def method_getter(name): return pysteps.postprocessing.interface.get_method(name, "ensemblestat") - invalid_names = ["ensemblestat_mean", "ensemblestat_excprob", "ensemblestat_banddepth"] + invalid_names = [ + "ensemblestat_mean", + "ensemblestat_excprob", + "ensemblestat_banddepth", + ] _generic_interface_test(method_getter, valid_names_func_pair, invalid_names) # Test diagnostics From f5df5a8e7faec095af3fbf0bbcf961e52f459075 Mon Sep 17 00:00:00 2001 From: Felix Erdmann Date: Thu, 27 Feb 2025 13:21:29 +0100 Subject: [PATCH 28/33] Cleaning as requested in ladc's review --- pysteps/postprocessing/diagnostics.py | 8 ++------ pysteps/postprocessing/ensemblestats.py | 8 -------- pysteps/postprocessing/interface.py | 26 +++---------------------- 3 files changed, 5 insertions(+), 37 deletions(-) diff --git a/pysteps/postprocessing/diagnostics.py b/pysteps/postprocessing/diagnostics.py index 333fb97ac..7b7f98a81 100644 --- a/pysteps/postprocessing/diagnostics.py +++ b/pysteps/postprocessing/diagnostics.py @@ -18,9 +18,5 @@ """ -# def diagnostic_example1(filename, **kwargs): -# return "Hello, I am an example diagnostics postprocessor." - - -# def diagnostic_example2(filename, **kwargs): -# return [[42, 42], [42, 42]] +# Add your diagnostic_ function here AND add this method to the _diagnostics_methods +# dictionary in postprocessing.interface.py diff --git a/pysteps/postprocessing/ensemblestats.py b/pysteps/postprocessing/ensemblestats.py index 1306a4325..351ec4a31 100644 --- a/pysteps/postprocessing/ensemblestats.py +++ b/pysteps/postprocessing/ensemblestats.py @@ -177,11 +177,3 @@ def banddepth(X, thr=None, norm=False): depth = (depth - depth.min()) / (depth.max() - depth.min()) return depth - - -# def ensemblestat_example1(filename, **kwargs): -# return "Hello, I am an example of postprocessing ensemble statistics." - - -# def ensemblestat_example2(filename, **kwargs): -# return [[42, 42], [42, 42]] diff --git a/pysteps/postprocessing/interface.py b/pysteps/postprocessing/interface.py index f71a1fb76..70426fbb2 100644 --- a/pysteps/postprocessing/interface.py +++ b/pysteps/postprocessing/interface.py @@ -205,32 +205,12 @@ def get_method(name, method_type): Name of the method. The available options are:\n diagnostics: - - .. tabularcolumns:: |p{2cm}|L| - - +---------------+-------------------------------------------------------+ - | Name | Description | - +===============+=======================================================+ - | Diagnostic | Example that returns a string | - | Example1 | | - +---------------+-------------------------------------------------------+ - | Diagnostic | Example that returns an array | - | Example3 | | - +---------------+-------------------------------------------------------+ + [nothing pre-installed] ensemblestats: + pre-installed: mean, excprob, banddepth - .. tabularcolumns:: |p{2cm}|L| - - +---------------+-------------------------------------------------------+ - | Name | Description | - +===============+=======================================================+ - | EnsembleStat | Example that returns a string | - | Example1 | | - +---------------+-------------------------------------------------------+ - | EnsembleStat | Example that returns an array | - | Example3 | | - +---------------+-------------------------------------------------------+ + Additional options might exist if plugins are installed. method_type: {'diagnostics', 'ensemblestats'} Type of the method (see tables above). From ccbb41fe22c3ccd665e77f6c2dd17855cae9c190 Mon Sep 17 00:00:00 2001 From: Felix Erdmann Date: Thu, 27 Feb 2025 13:26:22 +0100 Subject: [PATCH 29/33] Fix postprocessing interface test --- pysteps/tests/test_interfaces.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pysteps/tests/test_interfaces.py b/pysteps/tests/test_interfaces.py index ae4e80b92..1b99abab5 100644 --- a/pysteps/tests/test_interfaces.py +++ b/pysteps/tests/test_interfaces.py @@ -166,7 +166,7 @@ def test_postprocessing_interface(): ] def method_getter(name): - return pysteps.postprocessing.interface.get_method(name, "ensemblestat") + return pysteps.postprocessing.interface.get_method(name, "ensemblestats") invalid_names = [ "ensemblestat_mean", @@ -177,7 +177,7 @@ def method_getter(name): # Test diagnostics def method_getter(name): - return pysteps.io.interface.get_method(name, "diagnostic") + return pysteps.io.interface.get_method(name, "diagnostics") valid_names_func_pair = [] invalid_names = ["unknown"] From db83773cbb5a13c71dc5aa024b24a4726e329569 Mon Sep 17 00:00:00 2001 From: Felix Erdmann Date: Thu, 27 Feb 2025 14:48:33 +0100 Subject: [PATCH 30/33] Simplift postprocessing.interface and add more tests for it --- pysteps/postprocessing/interface.py | 31 ++++++++++++----------------- pysteps/tests/test_interfaces.py | 15 +++++++++++--- 2 files changed, 25 insertions(+), 21 deletions(-) diff --git a/pysteps/postprocessing/interface.py b/pysteps/postprocessing/interface.py index 70426fbb2..8358a5652 100644 --- a/pysteps/postprocessing/interface.py +++ b/pysteps/postprocessing/interface.py @@ -14,20 +14,21 @@ """ import importlib -from pysteps.postprocessing import diagnostics, ensemblestats +# from pysteps.postprocessing import diagnostics, ensemblestats +from pysteps import postprocessing as pp from pprint import pprint _diagnostics_methods = dict() _ensemblestats_methods = dict( - mean=ensemblestats.mean, - excprob=ensemblestats.excprob, - banddepth=ensemblestats.banddepth, + mean=pp.ensemblestats.mean, + excprob=pp.ensemblestats.excprob, + banddepth=pp.ensemblestats.banddepth, ) def add_postprocessor( - postprocessors_function_name, _postprocessors, methods_dict, module, attributes + postprocessors_function_name, _postprocessors, module, attributes ): """ Add the postprocessor to the appropriate _methods dictionary and to the module. @@ -38,10 +39,13 @@ def add_postprocessor( for example, e.g. diagnostic_example1 _postprocessors: function the function to be added - @param methods_dict: the dictionary where the function is added @param module: the module where the function is added, e.g. 'diagnostics' @param attributes: the existing functions in the selected module """ + # the dictionary where the function is added + methods_dict = ( + _diagnostics_methods if "diagnostic" in module else _ensemblestats_methods + ) # get funtion name without mo short_name = postprocessors_function_name.replace(f"{module}_", "") @@ -88,20 +92,11 @@ def discover_postprocessors(): postprocessors_function_name = _postprocessors.__name__ - if "diagnostic" in entry_point.module_name: - add_postprocessor( - postprocessors_function_name, - _postprocessors, - _diagnostics_methods, - "diagnostics", - entry_point.attrs, - ) - elif "ensemblestat" in entry_point.module_name: + if plugintype in entry_point.module_name: add_postprocessor( postprocessors_function_name, _postprocessors, - _ensemblestats_methods, - "ensemblestats", + f"{plugintype}s", entry_point.attrs, ) else: @@ -128,7 +123,7 @@ def print_postprocessors_info(module_name, interface_methods, module_methods): pprint(module_methods) print( - f"\npostprocessors available in the pysteps.postprocessing.get_method interface" + "\npostprocessors available in the pysteps.postprocessing.get_method interface" ) pprint([(short_name, f.__name__) for short_name, f in interface_methods.items()]) diff --git a/pysteps/tests/test_interfaces.py b/pysteps/tests/test_interfaces.py index 1b99abab5..b312c60be 100644 --- a/pysteps/tests/test_interfaces.py +++ b/pysteps/tests/test_interfaces.py @@ -165,6 +165,16 @@ def test_postprocessing_interface(): ("banddepth", banddepth), ] + # Test for exisiting functions + with pytest.raises(RuntimeWarning): + pysteps.postprocessing.interface.add_postprocessor( + "excprob", + "ensemblestat_excprob", + "ensemblestats", + [tup[0] for tup in valid_names_func_pair], + ) + + # Test get method for valid and invalid names def method_getter(name): return pysteps.postprocessing.interface.get_method(name, "ensemblestats") @@ -177,7 +187,7 @@ def method_getter(name): # Test diagnostics def method_getter(name): - return pysteps.io.interface.get_method(name, "diagnostics") + return pysteps.postprocessing.interface.get_method(name, "diagnostics") valid_names_func_pair = [] invalid_names = ["unknown"] @@ -187,8 +197,7 @@ def method_getter(name): # Test for invalid argument type with pytest.raises(TypeError): pysteps.postprocessing.interface.get_method("mean", None) - pysteps.postprocessing.interface.get_method(None, "ensemblestat") - pysteps.postprocessing.interface.get_method(None, "diagnostic") + pysteps.postprocessing.interface.get_method(None, "ensemblestats") # Test for invalid method types with pytest.raises(ValueError): From fa97a355f592ad01f6d53495de93d1ab95d45169 Mon Sep 17 00:00:00 2001 From: Felix Erdmann Date: Thu, 27 Feb 2025 14:57:45 +0100 Subject: [PATCH 31/33] Fix postprocessing.interface test to match expected warning --- pysteps/tests/test_interfaces.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pysteps/tests/test_interfaces.py b/pysteps/tests/test_interfaces.py index b312c60be..7e6344006 100644 --- a/pysteps/tests/test_interfaces.py +++ b/pysteps/tests/test_interfaces.py @@ -166,7 +166,7 @@ def test_postprocessing_interface(): ] # Test for exisiting functions - with pytest.raises(RuntimeWarning): + with pytest.warns(RuntimeWarning): pysteps.postprocessing.interface.add_postprocessor( "excprob", "ensemblestat_excprob", From 444331a9803ff40d2bdcfdffa848525595b9df4b Mon Sep 17 00:00:00 2001 From: Felix Erdmann Date: Thu, 27 Feb 2025 15:28:21 +0100 Subject: [PATCH 32/33] Bug fixes to run tests --- pysteps/postprocessing/interface.py | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/pysteps/postprocessing/interface.py b/pysteps/postprocessing/interface.py index 8358a5652..63ab5684e 100644 --- a/pysteps/postprocessing/interface.py +++ b/pysteps/postprocessing/interface.py @@ -14,16 +14,16 @@ """ import importlib -# from pysteps.postprocessing import diagnostics, ensemblestats -from pysteps import postprocessing as pp +from pysteps.postprocessing import diagnostics, ensemblestats from pprint import pprint +import warnings _diagnostics_methods = dict() _ensemblestats_methods = dict( - mean=pp.ensemblestats.mean, - excprob=pp.ensemblestats.excprob, - banddepth=pp.ensemblestats.banddepth, + mean=ensemblestats.mean, + excprob=ensemblestats.excprob, + banddepth=ensemblestats.banddepth, ) @@ -52,17 +52,19 @@ def add_postprocessor( if short_name not in methods_dict: methods_dict[short_name] = _postprocessors else: - RuntimeWarning( + warnings.warn( f"The {module} identifier '{short_name}' is already available in " f"'pysteps.postprocessing.interface_{module}_methods'.\n" - f"Skipping {module}:{'.'.join(attributes)}" + f"Skipping {module}:{'.'.join(attributes)}", + RuntimeWarning, ) if hasattr(globals()[module], postprocessors_function_name): - RuntimeWarning( + warnings.warn( f"The {module} function '{short_name}' is already an attribute" f"of 'pysteps.postprocessing.{module}'.\n" - f"Skipping {module}:{'.'.join(attributes)}" + f"Skipping {module}:{'.'.join(attributes)}", + RuntimeWarning, ) else: setattr(globals()[module], postprocessors_function_name, _postprocessors) From 657e56e3066046a906e49026acc5e656becf8dae Mon Sep 17 00:00:00 2001 From: Felix Erdmann Date: Thu, 27 Feb 2025 19:08:10 +0100 Subject: [PATCH 33/33] more test - codecov --- pysteps/postprocessing/interface.py | 8 ++++---- pysteps/tests/test_interfaces.py | 4 ++++ 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/pysteps/postprocessing/interface.py b/pysteps/postprocessing/interface.py index 63ab5684e..97b960539 100644 --- a/pysteps/postprocessing/interface.py +++ b/pysteps/postprocessing/interface.py @@ -5,6 +5,10 @@ Interface for the postprocessing module. +Support postprocessing types: + - ensmeblestats + - diagnostics + .. currentmodule:: pysteps.postprocessing.interface .. autosummary:: @@ -101,10 +105,6 @@ def discover_postprocessors(): f"{plugintype}s", entry_point.attrs, ) - else: - raise ValueError( - f"Unknown module {entry_point.module_name} in the entrypoint {entry_point.name}" - ) def print_postprocessors_info(module_name, interface_methods, module_methods): diff --git a/pysteps/tests/test_interfaces.py b/pysteps/tests/test_interfaces.py index 7e6344006..bfe1d6683 100644 --- a/pysteps/tests/test_interfaces.py +++ b/pysteps/tests/test_interfaces.py @@ -197,12 +197,16 @@ def method_getter(name): # Test for invalid argument type with pytest.raises(TypeError): pysteps.postprocessing.interface.get_method("mean", None) + with pytest.raises(TypeError): pysteps.postprocessing.interface.get_method(None, "ensemblestats") # Test for invalid method types with pytest.raises(ValueError): pysteps.postprocessing.interface.get_method("mean", "forecast") + # Test print + pysteps.postprocessing.postprocessors_info() + def test_motion_interface(): """Test the motion module interface."""