-
Notifications
You must be signed in to change notification settings - Fork 171
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
base: master
Are you sure you want to change the base?
Changes from all commits
f2c7514
11abb0d
36741db
9e79882
5abe467
092557c
15de2a4
c6ca85d
f5558ac
e64b70b
6605643
2ed3411
01bf7ba
3586f56
f581846
cb89616
f9bf9cf
13922fc
cb2529c
c1c4817
198df51
8c905f2
1305e88
296694d
b45c9a7
a20ce17
a4e0b6a
f340085
06d181a
17397e5
f5df5a8
ccbb41f
db83773
fa97a35
444331a
657e56e
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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( | ||
|
@@ -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__ | ||
|
@@ -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) | ||
|
@@ -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() | ||
] | ||
) | ||
|
||
|
@@ -107,7 +113,7 @@ def importers_info(): | |
|
||
difference = available_importers ^ importers_in_the_interface | ||
if len(difference) > 0: | ||
print("\nIMPORTANT:") | ||
# print("\nIMPORTANT:") | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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( | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,22 @@ | ||
""" | ||
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/ | ||
|
||
""" | ||
|
||
# Add your diagnostic_ function here AND add this method to the _diagnostics_methods | ||
# dictionary in postprocessing.interface.py | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Let's see how we proceed with this file. I think we must keep it as mention in Originally posted by @ladc in #405 (comment) |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,254 @@ | ||
# -*- coding: utf-8 -*- | ||
""" | ||
pysteps.postprocessing.interface | ||
==================== | ||
|
||
Interface for the postprocessing module. | ||
|
||
Support postprocessing types: | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I have added that we currently support these two types of postprocessing plugins. Note that we have removed the ensemblestats type from the cookiecutter at the moment. |
||
- ensmeblestats | ||
- diagnostics | ||
|
||
.. currentmodule:: pysteps.postprocessing.interface | ||
|
||
.. autosummary:: | ||
:toctree: ../generated/ | ||
|
||
get_method | ||
""" | ||
import importlib | ||
|
||
from pysteps.postprocessing import diagnostics, ensemblestats | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Codacy Static Code Analysis does not like the import of diagnostics as it isn't explicitly called. However, it is needed to add diagnostics plugins as |
||
from pprint import pprint | ||
import warnings | ||
|
||
_diagnostics_methods = dict() | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There is still a discrepancy here, as the dictionary in the |
||
|
||
_ensemblestats_methods = dict( | ||
mean=ensemblestats.mean, | ||
excprob=ensemblestats.excprob, | ||
banddepth=ensemblestats.banddepth, | ||
) | ||
|
||
|
||
def add_postprocessor( | ||
postprocessors_function_name, _postprocessors, module, attributes | ||
): | ||
""" | ||
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 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}_", "") | ||
if short_name not in methods_dict: | ||
methods_dict[short_name] = _postprocessors | ||
else: | ||
warnings.warn( | ||
f"The {module} identifier '{short_name}' is already available in " | ||
f"'pysteps.postprocessing.interface_{module}_methods'.\n" | ||
f"Skipping {module}:{'.'.join(attributes)}", | ||
RuntimeWarning, | ||
) | ||
|
||
if hasattr(globals()[module], postprocessors_function_name): | ||
warnings.warn( | ||
f"The {module} function '{short_name}' is already an attribute" | ||
f"of 'pysteps.postprocessing.{module}'.\n" | ||
f"Skipping {module}:{'.'.join(attributes)}", | ||
RuntimeWarning, | ||
) | ||
else: | ||
setattr(globals()[module], postprocessors_function_name, _postprocessors) | ||
|
||
|
||
def discover_postprocessors(): | ||
""" | ||
Search for installed postprocessing plugins in the entrypoint 'pysteps.plugins.postprocessors' | ||
|
||
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 | ||
# the execution of the python application. For example, when the plugins are | ||
# installed during the tests | ||
import pkg_resources | ||
|
||
importlib.reload(pkg_resources) | ||
|
||
# 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 plugintype in entry_point.module_name: | ||
add_postprocessor( | ||
postprocessors_function_name, | ||
_postprocessors, | ||
f"{plugintype}s", | ||
entry_point.attrs, | ||
) | ||
|
||
|
||
def print_postprocessors_info(module_name, interface_methods, module_methods): | ||
""" | ||
Helper function to print the postprocessors available in the module and in the interface. | ||
|
||
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 'diagnostic_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 interface_methods.items()]) | ||
|
||
module_methods_set = set(module_methods) | ||
interface_methods_set = set(interface_methods.keys()) | ||
|
||
difference = module_methods_set ^ interface_methods_set | ||
if len(difference) > 0: | ||
# print("\nIMPORTANT:") | ||
_diff = module_methods_set - interface_methods_set | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Since we want to avoid differences, it should not be a problem that these lines with |
||
if len(_diff) > 0: | ||
print( | ||
f"\nIMPORTANT:\nThe following postprocessors are available in {module_name} module but not in the pysteps.postprocessing.get_method interface" | ||
) | ||
pprint(_diff) | ||
_diff = interface_methods_set - module_methods_set | ||
if len(_diff) > 0: | ||
print( | ||
"\nWARNING:\n" | ||
f"The following postprocessors are available in the pysteps.postprocessing.get_method interface but not in the {module_name} module" | ||
) | ||
pprint(_diff) | ||
|
||
|
||
def postprocessors_info(): | ||
"""Print all the available postprocessors.""" | ||
|
||
available_postprocessors = set() | ||
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 | ||
for attr in dir(importlib.import_module(module_name)) | ||
if attr.startswith(plugintype[:-1]) | ||
] | ||
# 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_") | ||
] | ||
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 | ||
|
||
|
||
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 | ||
|
||
diagnostics: | ||
[nothing pre-installed] | ||
|
||
ensemblestats: | ||
pre-installed: mean, excprob, banddepth | ||
|
||
Additional options might exist if plugins are installed. | ||
|
||
method_type: {'diagnostics', 'ensemblestats'} | ||
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: 'diagnostics', 'ensemblestats'" | ||
) from None | ||
|
||
if isinstance(name, str): | ||
name = name.lower() | ||
else: | ||
raise TypeError( | ||
"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', 'ensemblestats'" | ||
) 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 |
There was a problem hiding this comment.
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?