Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adapt pysteps to allow for postprocessing plugins #405

Open
wants to merge 36 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 28 commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
f2c7514
Added structure for the creation of postprocessing plugins
joeycasey87 Jul 19, 2024
11abb0d
Update interface.py
joeycasey87 Jul 25, 2024
36741db
Updated to fit black formatting
joeycasey87 Jul 25, 2024
9e79882
Update interface.py
joeycasey87 Jul 25, 2024
5abe467
Update diagnostics.py
joeycasey87 Jul 25, 2024
092557c
Added tests to improve code coverage
joeycasey87 Aug 13, 2024
15de2a4
Remove redundant import and getattr statements
ladc Aug 20, 2024
c6ca85d
Adjusted so that the interface is more easily interpretable
joeycasey87 Aug 26, 2024
f5558ac
Fixed error from stashed chages
joeycasey87 Aug 26, 2024
e64b70b
update test_pysteps
joeycasey87 Aug 26, 2024
6605643
Remove try-import statements for diagnostics.py
ladc Aug 26, 2024
2ed3411
Revert back to master and Python 3.10 in check_black.yml
ladc Aug 26, 2024
01bf7ba
Make sure the postprocessors are actually discovered in the main init
ladc Aug 26, 2024
3586f56
Refactor code for postprocessing plugin detection.
ladc Aug 27, 2024
f581846
Update dummy code names for new plugin structure
ladc Jan 28, 2025
cb89616
Change postprocessor interface to use diagnostic_ and ensemblestat_ p…
ladc Jan 28, 2025
f9bf9cf
Fix the postprocessing interface
Jan 29, 2025
13922fc
Update postprocessing package to work with plugins
Jan 29, 2025
cb2529c
Update both io interface and postprocessign interface
Jan 30, 2025
c1c4817
Merge branch 'master' into postprocessor_plugin
FelixE91 Feb 25, 2025
198df51
Reformatted files with pre-commit
Feb 25, 2025
8c905f2
Merge branch 'postprocessor_plugin' of github.com:pySTEPS/pysteps int…
Feb 25, 2025
1305e88
Remove tests for diagnostics plugins interfaces.
ladc Feb 25, 2025
296694d
Fix io.interface to work with the new cookiecutter
Feb 26, 2025
b45c9a7
Merge branch 'master' into postprocessor_plugin
ladc Feb 26, 2025
a20ce17
Test if the default cookiecutter plugin can be loaded in the CI tests.
ladc Feb 26, 2025
a4e0b6a
Fix default plugin path in tox.ini.
ladc Feb 26, 2025
f340085
Fix plugin tests to use new cookiecutter template; try out importer a…
ladc Feb 26, 2025
06d181a
Added postprocessing module interface test
Feb 27, 2025
17397e5
Postprocessing interface reformatted with pre-commit
Feb 27, 2025
f5df5a8
Cleaning as requested in ladc's review
Feb 27, 2025
ccbb41f
Fix postprocessing interface test
Feb 27, 2025
db83773
Simplift postprocessing.interface and add more tests for it
Feb 27, 2025
fa97a35
Fix postprocessing.interface test to match expected warning
Feb 27, 2025
444331a
Bug fixes to run tests
Feb 27, 2025
657e56e
more test - codecov
Feb 27, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 4 additions & 8 deletions ci/test_plugin_support.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should similar lines be added to test the diagnostics plugin? Or not needed here in ci?


import_institution_name("filename")
print("PASSED")
1 change: 1 addition & 0 deletions pysteps/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
28 changes: 17 additions & 11 deletions pysteps/io/interface.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,8 @@
"""
import importlib

from importlib.metadata import 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(
Expand Down Expand Up @@ -49,7 +46,16 @@ def discover_importers():
The importers found are added to the `pysteps.io.interface_importer_methods`
dictionary containing the available importers.
"""
for entry_point in entry_points(group="pysteps.plugins.importers"):
# The pkg resources needs to be reload 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.importer", name=None
):
_importer = entry_point.load()

importer_function_name = _importer.__name__
Expand All @@ -63,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)
Expand All @@ -81,22 +87,22 @@ 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")
pprint(available_importers)

# 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()
]
)

Expand All @@ -107,7 +113,7 @@ def importers_info():

difference = available_importers ^ importers_in_the_interface
if len(difference) > 0:
print("\nIMPORTANT:")
# print("\nIMPORTANT:")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Delete?

_diff = available_importers - importers_in_the_interface
if len(_diff) > 0:
print(
Expand Down
3 changes: 3 additions & 0 deletions pysteps/postprocessing/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,6 @@
"""Methods for post-processing of forecasts."""

from . import ensemblestats
from .diagnostics import *
from .interface import *
from .ensemblestats import *
26 changes: 26 additions & 0 deletions pysteps/postprocessing/diagnostics.py
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could we just delete this file?

Copy link
Contributor

@FelixE91 FelixE91 Feb 27, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think so, at least not until we change the structure. The objective was to create a structure similar to what exists for the importers with io.importers.py and for the ensemblestats with postprocessing.ensemblestats.py.
The postprocessing interface.py and __init__.py currently read from diagnostics.py to check for available pre-installed functions1 - that's consistent with the way pre-installed ensemblestats functions are loaded and similar to the io.__init__.py and io.interface.py to load pre-installed importers.

New functions found in the entry_points are added to the available methods in the methods dictionary.

The (io and postprocessing) interface also reads from the module (importers.py and ensemblestats.py|diagnostics.py, respectively) BUT it would not add the functions to the methods dictionary. Instead, they must be added explicitly to the interface.py methods dictionary (_importer_methods and _ensemblestats_methods|_diagnostics_methods, respectively) to avoid an error in the get_method() function.
This might be fixed to add pre-installed functions from the module automatically to the methods dictionary.

Footnotes

  1. At the moment there are no pre-installed diagnostics. This might change in the future, unless we always use the cookiecutter to create plugins

Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
"""
pysteps.postprocessing.diagnostics
====================

Methods for applying diagnostics postprocessing.

The methods in this module implement the following interface::

diagnostic_xxx(optional arguments)

where **xxx** is the name of the diagnostic to be applied.

Available Diagnostics Postprocessors
------------------------

.. autosummary::
:toctree: ../generated/

"""

# def diagnostic_example1(filename, **kwargs):
# return "Hello, I am an example diagnostics postprocessor."


# def diagnostic_example2(filename, **kwargs):
# return [[42, 42], [42, 42]]
8 changes: 8 additions & 0 deletions pysteps/postprocessing/ensemblestats.py
Original file line number Diff line number Diff line change
Expand Up @@ -177,3 +177,11 @@ 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]]
Loading