Skip to content

Commit

Permalink
Improve the hooks
Browse files Browse the repository at this point in the history
  • Loading branch information
frode-aarstad committed Oct 10, 2024
1 parent cbcaa3a commit 24e50da
Show file tree
Hide file tree
Showing 7 changed files with 20 additions and 234 deletions.
119 changes: 8 additions & 111 deletions src/ert/shared/_doc_utils/everest_jobs.py
Original file line number Diff line number Diff line change
@@ -1,19 +1,12 @@
from __future__ import annotations

from collections import defaultdict
from typing import Any, ClassVar, Dict, List, Union
from typing import Any, ClassVar, Dict, List

from docutils import nodes
from sphinx.util.docutils import SphinxDirective

from ert.config.forward_model_step import ForwardModelStepDocumentation
from ert.plugins import ErtPluginManager, JobDoc

from everest.plugins.everest_plugin_manager import EverestPluginManager

from ert.shared._doc_utils.forward_model_documentation import _ForwardModelDocumentation


from . import _create_section_with_title


Expand All @@ -32,124 +25,28 @@ class _EverestDocumentation(SphinxDirective):
_EXAMPLES_DEFAULT = ""
_PARSER_DEFAULT = None

@staticmethod
def _divide_into_categories(
jobs: Union[
Dict[str, JobDoc], Dict[str, Union[ForwardModelStepDocumentation, JobDoc]]
],
) -> Dict[str, Dict[str, List[_ForwardModelDocumentation]]]:
categories: Dict[str, Dict[str, List[_ForwardModelDocumentation]]] = (
defaultdict(lambda: defaultdict(list))
)
for job_name, docs in jobs.items():
# Job names in ERT traditionally used upper case letters
# for the names of the job. However, at some point duplicate
# jobs where added with lower case letters for some of the scripts.
# To avoid duplicate entries in the documentation, lower case
# job names are skipped.
if job_name.islower():
continue

if isinstance(docs, ForwardModelStepDocumentation):
docs = {
"description": docs.description,
"examples": docs.examples,
"config_file": docs.config_file,
"parser": None,
"source_package": docs.source_package,
"category": docs.category,
}

category = docs.get(
"category",
_EverestDocumentation._CATEGORY_DEFAULT,
)

split_categories = category.split(".")
if len(split_categories) > 1:
main_category, sub_category = split_categories[0:2]
elif len(split_categories) == 1:
main_category, sub_category = split_categories[0], "other"
else:
main_category, sub_category = "other", "other"

categories[main_category][sub_category].append(
_ForwardModelDocumentation(
name=job_name,
category=category,
job_source=docs.get(
"source_package",
_EverestDocumentation._SOURCE_PACKAGE_DEFAULT,
),
description=docs.get(
"description",
_EverestDocumentation._DESCRIPTION_DEFAULT,
),
job_config_file=docs.get(
"config_file",
_EverestDocumentation._CONFIG_FILE_DEFAULT,
),
examples=docs.get(
"examples",
_EverestDocumentation._EXAMPLES_DEFAULT,
),
parser=docs.get("parser", _EverestDocumentation._PARSER_DEFAULT),
)
)

return {k: dict(v) for k, v in categories.items()}

def _generate_job_documentation_without_title(
self,
jobs: Union[
Dict[str, JobDoc], Dict[str, Union[ForwardModelStepDocumentation, JobDoc]]
],
docs: Dict[str, Any],
) -> List[nodes.section]:
job_categories = _EverestDocumentation._divide_into_categories(jobs)
node_list = []

for category_index, category in enumerate(sorted(job_categories.keys())):
for job_name, job_doc in dict(sorted(docs.items())).items():
category_section_node = _create_section_with_title(
section_id=category + "-category", title=category.capitalize()
section_id=job_name + "-category", title=job_doc["full_job_name"]
)
sub_jobs_map = job_categories[category]
for sub_i, sub in enumerate(sorted(sub_jobs_map.keys())):
sub_section_node = _create_section_with_title(
section_id=category + "-" + sub + "-subcategory",
title=sub.capitalize(),
)

for job in sub_jobs_map[sub]:
job_section_node = job.create_node(self.state)
sub_section_node.append(job_section_node)

# A section is not allowed to end with a transition,
# so we don't add after the last sub-category
if sub_i < len(sub_jobs_map) - 1:
sub_section_node.append(nodes.transition())

category_section_node.append(sub_section_node)

node_list.append(category_section_node)
n = nodes.literal_block(text=job_doc["help"])
n["xml:space"] = "preserve"
node_list.append(n)

# A section is not allowed to end with a transition,
# so we don't add after the last category
if category_index < len(job_categories) - 1:
category_section_node.append(nodes.transition())
return node_list



class EverestForwardModelDocumentation(_EverestDocumentation):
pm = EverestPluginManager()
_JOBS: ClassVar[dict[str, Any]] = {
**pm.get_documentation_for_jobs(),
**pm.get_documentation_for_forward_model_steps(),
}
_JOBS: ClassVar[dict[str, Any]] = {**pm.get_documentation()}

def run(self) -> List[nodes.section]:
return self._generate_job_documentation_without_title(
EverestForwardModelDocumentation._JOBS,
)


6 changes: 1 addition & 5 deletions src/ert/shared/_doc_utils/forward_model_documentation.py
Original file line number Diff line number Diff line change
@@ -1,17 +1,13 @@

from __future__ import annotations

from argparse import ArgumentParser
from typing import Any, Callable, Optional

from typing import Any, Callable, Optional

from docutils import nodes


from . import _create_section_with_title, _parse_raw_rst



class _ForwardModelDocumentation:
def __init__(
self,
Expand Down
1 change: 0 additions & 1 deletion src/everest/plugins/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
from everest.plugins.plugin_response import plugin_response
from everest.strings import EVEREST


hookimpl = pluggy.HookimplMarker(EVEREST)
hookspec = pluggy.HookspecMarker(EVEREST)

Expand Down
103 changes: 4 additions & 99 deletions src/everest/plugins/everest_plugin_manager.py
Original file line number Diff line number Diff line change
@@ -1,19 +1,9 @@
from typing import Any, Dict, List, Tuple, Type, TypeVar, Union
from typing import Any, Dict

import pluggy

from ert.config.forward_model_step import ForwardModelStepDocumentation, ForwardModelStepPlugin
from everest.plugins import hook_impl, hook_specs
from everest.strings import EVEREST
import logging
from .plugin_response import PluginMetadata, PluginResponse

logger = logging.getLogger(__name__)




K = TypeVar("K")
V = TypeVar("V")


class EverestPluginManager(pluggy.PluginManager):
Expand All @@ -27,90 +17,5 @@ def __init__(self, plugins=None):
for plugin in plugins:
self.register(plugin)

@property
def forward_model_steps(
self,
) -> List[Type[ForwardModelStepPlugin]]:
fm_steps_listed = [
resp.data for resp in self.hook.installable_forward_model_steps()
]
return [fm_step for fm_steps in fm_steps_listed for fm_step in fm_steps]


@staticmethod
def _add_plugin_info_to_dict(
d: Dict[K, V], plugin_response: PluginResponse[Any]
) -> Dict[K, Tuple[V, PluginMetadata]]:
return {k: (v, plugin_response.plugin_metadata) for k, v in d.items()}

@staticmethod
def _merge_dicts(
list_of_dicts: List[PluginResponse[Dict[str, V]]],
include_plugin_data: bool = False,
) -> Union[Dict[str, V], Dict[str, Tuple[V, PluginMetadata]]]:
list_of_dicts.reverse()
merged_dict: Dict[str, Tuple[V, PluginMetadata]] = {}
for d in list_of_dicts:
conflicting_keys = set(merged_dict.keys()) & set(d.data.keys())
for ck in conflicting_keys:
logger.info(
f"Overwriting {ck} from "
f"{merged_dict[ck][1].plugin_name}"
f"({merged_dict[ck][1].function_name}) "
f"with data from {d.plugin_metadata.plugin_name}"
f"({d.plugin_metadata.function_name})"
)
merged_dict.update(EverestPluginManager._add_plugin_info_to_dict(d.data, d))
if include_plugin_data:
return merged_dict
return {k: v[0] for k, v in merged_dict.items()}

@staticmethod
def _evaluate_job_doc_hook(
hook: pluggy.HookCaller, job_name: str
) -> Dict[Any, Any]:
response = hook(job_name=job_name)

if response is None:
logger.debug(f"Got no documentation for {job_name} from any plugins")
return {}

return response.data



def get_documentation_for_jobs(self) -> Dict[str, Any]:
print(self.hook.get_forward_models())
job_docs = {
k: {
"config_file": v[0],
"source_package": v[1].plugin_name,
"source_function_name": v[1].function_name,
}

for k, v in EverestPluginManager._merge_dicts(
self.hook.installable_jobs(), include_plugin_data=True
).items()
}
for key, value in job_docs.items():
value.update(
EverestPluginManager._evaluate_job_doc_hook(
self.hook.job_documentation,
key,
)
)
return job_docs

def get_documentation_for_forward_model_steps(
self,
) -> Dict[str, ForwardModelStepDocumentation]:
return {
# Implementations of plugin fm step take no __init__ args
# (name, command)
# but mypy expects the subclasses to take in same arguments upon
# initializations
fm_step().name: fm_step.documentation() # type: ignore
for fm_step in self.forward_model_steps
if fm_step.documentation() is not None
}

def get_documentation(self) -> Dict[str, Any]:
return self.hook.get_forward_model_documentations()[0]
5 changes: 1 addition & 4 deletions src/everest/plugins/hook_impl.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,10 +59,7 @@ def get_forward_models_schemas():
def installable_workflow_jobs():
return None

@hookimpl
def installable_jobs():
return None

@hookimpl
def installable_forward_model_steps():
def get_forward_model_documentations():
return None
12 changes: 2 additions & 10 deletions src/everest/plugins/hook_specs.py
Original file line number Diff line number Diff line change
Expand Up @@ -108,15 +108,7 @@ def add_log_handle_to_root():
:rtype: logging.Handler
"""

@hookspec
def installable_jobs():
"""
"""

@hookspec
def installable_forward_model_steps():
"""
"""

def get_forward_model_documentations():
""" """
8 changes: 4 additions & 4 deletions src/everest/plugins/plugin_response.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
from typing import Generic, TypeVar

from decorator import decorator

from typing import Generic, TypeVar
T = TypeVar("T")


@decorator
def plugin_response(func, plugin_name="", *args, **kwargs): # pylint: disable=keyword-arg-before-vararg
response = func(*args, **kwargs)
Expand All @@ -13,15 +15,13 @@ def plugin_response(func, plugin_name="", *args, **kwargs): # pylint: disable=k
)




class PluginMetadata:
def __init__(self, plugin_name, function_name):
self.plugin_name = plugin_name
self.function_name = function_name


class PluginResponse(Generic[T]):
def __init__(self, data: T, plugin_metadata: PluginMetadata) -> None:
self.data = data
self.plugin_metadata = plugin_metadata

0 comments on commit 24e50da

Please sign in to comment.