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

Improve yaml models: ConfigTasks #122

Merged
merged 11 commits into from
Feb 13, 2025
25 changes: 23 additions & 2 deletions src/sirocco/core/_tasks/icon_task.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,19 @@
import re
from dataclasses import dataclass, field
from pathlib import Path
from typing import TYPE_CHECKING, Any, Self

import f90nml

from sirocco.core.graph_items import Task
from sirocco.parsing._yaml_data_models import ConfigIconTaskSpecs
from sirocco.parsing import yaml_data_models as models

if TYPE_CHECKING:
from sirocco.parsing.yaml_data_models import ConfigTask


@dataclass(kw_only=True)
class IconTask(ConfigIconTaskSpecs, Task):
class IconTask(models.ConfigIconTaskSpecs, Task):
core_namelists: dict[str, f90nml.Namelist] = field(default_factory=dict)

def init_core_namelists(self):
Expand Down Expand Up @@ -93,3 +97,20 @@ def section_index(section_name) -> tuple[str, int | None]:
if m := multi_section_pattern.match(section_name):
return m.group(1), int(m.group(2)) - 1
return section_name, None

@classmethod
def build_from_config(cls: type[Self], config: ConfigTask, **kwargs: Any) -> Self:
Copy link
Collaborator

Choose a reason for hiding this comment

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

Also, just for my understanding: We're supporting Python >= 3.10, but Self type was introduced in 3.11. Is that an issue, or can we use newer Python features in type hinting? According to ChatGPT:

"""
You're correct—Self was introduced in Python 3.11, not 3.12. Since your package claims to support Python 3.10+, using Self directly would be a problem for users on 3.10.

Can you use newer Python concepts in type annotations?
Yes! Starting with PEP 563 (Python 3.7) and PEP 649 (Python 3.10), Python supports postponed evaluation of annotations, meaning type hints are stored as strings instead of being evaluated immediately. This allows you to use newer type hints while keeping compatibility with older versions, as long as they aren't used at runtime.

Solution:
If you add this import at the top of your module:
from __future__ import annotations
Then the Self annotation will be treated as a string ("Self") and won't cause syntax errors in Python 3.10. However, tools like mypy will still understand it.
"""

Copy link
Collaborator Author

@DropD DropD Feb 13, 2025

Choose a reason for hiding this comment

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

GPT is correct in that it was introduced later and in that in our case it works (at runtime) because it is treated as a string. Here's how that applies to Sirocco:

For now, we have set the hatch project to run type analysis on 3.12 and we use the future annotations. Therefore it is type checked as if it was supporting 3.12+ (ok at analysis time), while the interpreter treats all the type hints as just strings (ok at run time).

If we were to lower that to 3.10, we would have to import Self from typing_extensions (making that a dependency), if we choose to keep using it. It would also affect other things (not touched in this PR) like class Store[T], which would have to turn into class Store(typing.Generic[T]). The backports in typing_extensions can catch some of this but not syntax changes like class Store[T] (which I am honestly suprised that it works at runtime). I am also wondering why from typing import Self does not cause an import error in 3.10, but it seems not to.

config_kwargs = dict(config)
del config_kwargs["parameters"]
# The following check is here for type checkers.
# We don't want to narrow the type in the signature, as that would break liskov substitution.
# We guarantee elsewhere this is called with the correct type at runtime
if not isinstance(config, models.ConfigIconTask):
raise TypeError
Comment on lines +109 to +110
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

This check is here to satisfy mypy, because we can not put this as a type hint in the signature (would break Liskof substitution principle -> mypy complains). Not necessary for ShellTask because we really check in Task that the subclasses get the correct types.

config_kwargs["namelists"] = {
nml.path.name: models.NamelistSpec(**nml.model_dump()) for nml in config.namelists
}
return cls(
**kwargs,
**config_kwargs,
)
5 changes: 2 additions & 3 deletions src/sirocco/core/_tasks/shell_task.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,8 @@
from dataclasses import dataclass

from sirocco.core.graph_items import Task
from sirocco.parsing._yaml_data_models import ConfigShellTaskSpecs
from sirocco.parsing.yaml_data_models import ConfigShellTaskSpecs


@dataclass(kw_only=True)
class ShellTask(ConfigShellTaskSpecs, Task):
pass
class ShellTask(ConfigShellTaskSpecs, Task): ...
23 changes: 13 additions & 10 deletions src/sirocco/core/graph_items.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
from itertools import chain, product
from typing import TYPE_CHECKING, Any, ClassVar, Self, TypeAlias, TypeVar, cast

from sirocco.parsing._yaml_data_models import (
from sirocco.parsing.yaml_data_models import (
ConfigAvailableData,
ConfigBaseDataSpecs,
ConfigBaseTaskSpecs,
Expand All @@ -17,7 +17,7 @@

from termcolor._types import Color

from sirocco.parsing._yaml_data_models import (
from sirocco.parsing.yaml_data_models import (
ConfigBaseData,
ConfigCycleTask,
ConfigCycleTaskWaitOn,
Expand Down Expand Up @@ -66,7 +66,7 @@ def from_config(cls, config: ConfigBaseData, coordinates: dict) -> Self:
class Task(ConfigBaseTaskSpecs, GraphItem):
"""Internal representation of a task node"""

plugin_classes: ClassVar[dict[str, type]] = field(default={}, repr=False)
plugin_classes: ClassVar[dict[str, type[Self]]] = field(default={}, repr=False)
color: ClassVar[Color] = field(default="light_red", repr=False)

inputs: list[BoundData] = field(default_factory=list)
Expand All @@ -87,7 +87,7 @@ def __init_subclass__(cls, **kwargs):

@classmethod
def from_config(
cls,
cls: type[Self],
config: ConfigTask,
config_rootdir: Path,
start_date: datetime | None,
Expand All @@ -102,22 +102,19 @@ def from_config(
for data_node in datastore.iter_from_cycle_spec(input_spec, coordinates)
]
outputs = [datastore[output_spec.name, coordinates] for output_spec in graph_spec.outputs]
# use the fact that pydantic models can be turned into dicts easily
cls_config = dict(config)
del cls_config["parameters"]
if (plugin_cls := Task.plugin_classes.get(type(config).plugin, None)) is None:
msg = f"Plugin {type(config).plugin!r} is not supported."
raise ValueError(msg)

new = plugin_cls(
new = plugin_cls.build_from_config(
config,
config_rootdir=config_rootdir,
coordinates=coordinates,
start_date=start_date,
end_date=end_date,
inputs=inputs,
outputs=outputs,
**cls_config,
) # this works because dataclass has generated this init for us
)

# Store for actual linking in link_wait_on_tasks() once all tasks are created
new._wait_on_specs = graph_spec.wait_on # noqa: SLF001 we don't have access to self in a dataclass
Expand All @@ -126,6 +123,12 @@ def from_config(

return new

@classmethod
def build_from_config(cls: type[Self], config: ConfigTask, **kwargs: Any) -> Self:
config_kwargs = dict(config)
del config_kwargs["parameters"]
return cls(**kwargs, **config_kwargs)

def link_wait_on_tasks(self, taskstore: Store[Task]) -> None:
self.wait_on = list(
chain(
Expand Down
4 changes: 2 additions & 2 deletions src/sirocco/core/workflow.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
from typing import TYPE_CHECKING, Self

from sirocco.core.graph_items import Cycle, Data, Store, Task
from sirocco.parsing._yaml_data_models import (
from sirocco.parsing.yaml_data_models import (
ConfigBaseData,
ConfigWorkflow,
)
Expand All @@ -14,7 +14,7 @@
from datetime import datetime
from pathlib import Path

from sirocco.parsing._yaml_data_models import (
from sirocco.parsing.yaml_data_models import (
ConfigCycle,
ConfigData,
ConfigTask,
Expand Down
2 changes: 1 addition & 1 deletion src/sirocco/parsing/__init__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from ._yaml_data_models import (
from .yaml_data_models import (
ConfigWorkflow,
)

Expand Down
Loading