From 711434a7ddec6f6e97adf73695bf878bff9cc756 Mon Sep 17 00:00:00 2001 From: Sandor Kertesz Date: Wed, 15 Apr 2026 14:37:55 +0100 Subject: [PATCH] Use None for field ensemble member by default --- .pre-commit-config.yaml | 4 +-- src/earthkit/data/__init__.py | 13 +++----- src/earthkit/data/core/__init__.py | 3 +- src/earthkit/data/data/__init__.py | 16 +++++----- src/earthkit/data/encoders/__init__.py | 14 ++++---- src/earthkit/data/field/component/ensemble.py | 5 ++- src/earthkit/data/readers/__init__.py | 6 ++-- src/earthkit/data/sources/__init__.py | 4 +-- src/earthkit/data/targets/__init__.py | 32 +++++++++---------- tests/field/test_ensemble_component.py | 15 +++++++++ tests/grib/test_grib_ensemble.py | 11 +++++++ 11 files changed, 70 insertions(+), 53 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 450d54d2c..e676d7562 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -15,10 +15,10 @@ repos: - id: check-merge-conflict # Check for files that contain merge conflict exclude: /README\.rst$|^docs/.*\.rst$ - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.15.4 + rev: v0.15.10 hooks: - id: ruff-check - exclude: '(dev/.*|.*_)\.py$|docs/source/experimental/.*\.ipynb$|polytope_feature\.ipynb$|field_overview\.ipynb$' + exclude: '(dev/.*|_.*)\.py$' args: - --fix - --exit-non-zero-on-fix diff --git a/src/earthkit/data/__init__.py b/src/earthkit/data/__init__.py index fe2764b11..917f9bc5a 100644 --- a/src/earthkit/data/__init__.py +++ b/src/earthkit/data/__init__.py @@ -24,18 +24,13 @@ from .core.caching import CACHE as cache from .core.config import CONFIG as config from .core.field import Field -from .core.fieldlist import FieldList -from .core.fieldlist import create_fieldlist +from .core.fieldlist import FieldList, create_fieldlist from .encoders import create_encoder from .indexing.simple import SimpleFieldList -from .sources import Source -from .sources import from_source -from .sources import from_source_lazily -from .targets import create_target -from .targets import to_target +from .sources import Source, from_source, from_source_lazily +from .targets import create_target, to_target from .utils.concat import concat -from .utils.examples import download_example_file -from .utils.examples import remote_example_file +from .utils.examples import download_example_file, remote_example_file settings = config diff --git a/src/earthkit/data/core/__init__.py b/src/earthkit/data/core/__init__.py index df4f43ca8..3f8314cac 100644 --- a/src/earthkit/data/core/__init__.py +++ b/src/earthkit/data/core/__init__.py @@ -6,8 +6,7 @@ # nor does it submit to any jurisdiction. # -from abc import ABCMeta -from abc import abstractmethod +from abc import ABCMeta, abstractmethod class Base(metaclass=ABCMeta): diff --git a/src/earthkit/data/data/__init__.py b/src/earthkit/data/data/__init__.py index c097cbad0..cb47e19c0 100644 --- a/src/earthkit/data/data/__init__.py +++ b/src/earthkit/data/data/__init__.py @@ -11,18 +11,16 @@ from __future__ import annotations from abc import abstractmethod -from typing import TYPE_CHECKING -from typing import Any -from typing import TypeAlias +from typing import TYPE_CHECKING, Any, TypeAlias from earthkit.data.core import Encodable - if TYPE_CHECKING: + import numpy # type: ignore[import] + import pandas # type: ignore[import] import xarray # type: ignore[import] + from earthkit.data.core.fieldlist import FieldList # type: ignore[import] - import pandas # type: ignore[import] - import numpy # type: ignore[import] ArrayLike: TypeAlias = Any @@ -59,7 +57,8 @@ def is_stream(self) -> bool: Returns ------- bool - True if this data object represents a stream, False otherwise.""" + True if this data object represents a stream, False otherwise. + """ pass @abstractmethod @@ -133,7 +132,8 @@ def to_array(self, *args, **kwargs) -> ArrayLike: class SimpleData(Data): """A simple implementation of the Data interface that provides default implementations - for some methods.""" + for some methods. + """ def is_stream(self) -> bool: """Return False as this data object is not a stream.""" diff --git a/src/earthkit/data/encoders/__init__.py b/src/earthkit/data/encoders/__init__.py index 539611da9..0ed7850c5 100644 --- a/src/earthkit/data/encoders/__init__.py +++ b/src/earthkit/data/encoders/__init__.py @@ -204,24 +204,26 @@ def _encode_xarray(self, data, *, target=None, **kwargs) -> EncodedData: def _encode_featurelist(self, featurelist, *, target=None, **kwargs) -> EncodedData: """Subclass implementation of the encoding logic for a FeatureList. - Parameters: - ----------- + Parameters + ---------- featurelist: :obj:`FeatureList` The FeatureList to encode - Double dispatch method that called from ``featurelist`` to encode itself.""" + Double dispatch method that called from ``featurelist`` to encode itself. + """ pass @abstractmethod def _encode_path(self, path_info, *, target=None, **kwargs) -> EncodedData: """Subclass implementation of the encoding logic for a path. - Parameters: - ----------- + Parameters + ---------- path_info: :obj:`PathInfo` The PathInfo to encode - Double dispatch method that called from ``featurelist`` to encode itself.""" + Double dispatch method that called from ``featurelist`` to encode itself. + """ pass diff --git a/src/earthkit/data/field/component/ensemble.py b/src/earthkit/data/field/component/ensemble.py index f42854add..885dc8ea6 100644 --- a/src/earthkit/data/field/component/ensemble.py +++ b/src/earthkit/data/field/component/ensemble.py @@ -144,13 +144,12 @@ class Ensemble(EnsembleBase): ---------- member : str, int, optional The ensemble member, by default None. Internally stored as a string, - so if an integer is provided, it will be converted to a string. - None is treated as "0". + valid non-string values will be converted to a string. """ def __init__(self, member=None) -> None: if member is None: - self._member = "0" + self._member = None elif isinstance(member, int): self._member = str(member) else: diff --git a/src/earthkit/data/readers/__init__.py b/src/earthkit/data/readers/__init__.py index 1f6f49824..075408185 100644 --- a/src/earthkit/data/readers/__init__.py +++ b/src/earthkit/data/readers/__init__.py @@ -13,10 +13,10 @@ from abc import abstractmethod from importlib import import_module -from earthkit.data.core import Encodable -from earthkit.data.core import Loader +from earthkit.data.core import Encodable, Loader from earthkit.data.core.config import CONFIG -from earthkit.data.decorators import detect_out_filename, locked +from earthkit.data.decorators import detect_out_filename as detect_out_filename +from earthkit.data.decorators import locked LOG = logging.getLogger(__name__) diff --git a/src/earthkit/data/sources/__init__.py b/src/earthkit/data/sources/__init__.py index 0feaacff1..258724b98 100644 --- a/src/earthkit/data/sources/__init__.py +++ b/src/earthkit/data/sources/__init__.py @@ -13,7 +13,7 @@ from abc import abstractmethod from importlib import import_module -from earthkit.data.core import Encodable +from earthkit.data.core import Encodable as Encodable from earthkit.data.core import Loader from earthkit.data.core.caching import cache_file from earthkit.data.core.plugins import find_plugin @@ -66,8 +66,6 @@ def graph(self, depth=0): def to_data_object(self): """Convert this source into a data object, if possible.""" - from earthkit.data.data.source import DefaultSourceData - return DefaultSourceData(self) diff --git a/src/earthkit/data/targets/__init__.py b/src/earthkit/data/targets/__init__.py index 1013b761e..f1a12be3c 100644 --- a/src/earthkit/data/targets/__init__.py +++ b/src/earthkit/data/targets/__init__.py @@ -9,8 +9,7 @@ import logging import os -from abc import ABCMeta -from abc import abstractmethod +from abc import ABCMeta, abstractmethod from functools import lru_cache from importlib import import_module @@ -21,8 +20,8 @@ class Target(metaclass=ABCMeta): """ Represent a target. - Parameters: - ----------- + Parameters + ---------- encoder: str, Encoder, None The encoder to use to encode the data. Can be overridden in the the :obj:`write` method. When a string is passed, the encoder is looked up in the available encoders. When None, @@ -57,8 +56,8 @@ def write( """ Write data to the target using the given encoder. - Parameters: - ----------- + Parameters + ---------- data: obj, None The data object to write. If None, the encoder will use all the other arguments to generate the data to write. @@ -76,8 +75,8 @@ def write( **kwargs: dict Other keyword arguments passed to the encoder. - Raises: - ------- + Raises + ------ ValueError: If the target is already closed. """ pass @@ -90,8 +89,8 @@ def close(self): It must also call :obj:`_mark_closed`. The target will not be able to write anymore. - Raises: - ------- + Raises + ------ ValueError: If the target is already closed. """ pass @@ -102,8 +101,8 @@ def flush(self): Some targets may require to flush the data to the underlying storage. - Raises: - ------- + Raises + ------ ValueError: If the target is already closed. """ pass @@ -153,8 +152,8 @@ def write( def _write(self, data, **kwargs): """Write generic data to the target. - Parameters: - ----------- + Parameters + ---------- data: Data to write to the target. """ @@ -257,12 +256,11 @@ def to_target(target, *args, **kwargs): This is a top level function that writes data to a target. - Parameters: - ----------- + Parameters + ---------- target: str The target to write to. Must be a string. """ - with create_target(target, *args, **kwargs) as t: for k in [*target_kwargs(type(t)), *target_kwargs(Target)]: kwargs.pop(k, None) diff --git a/tests/field/test_ensemble_component.py b/tests/field/test_ensemble_component.py index 7c90b98ef..b453e4f22 100644 --- a/tests/field/test_ensemble_component.py +++ b/tests/field/test_ensemble_component.py @@ -21,6 +21,13 @@ def test_ensemble_component_alias_1(): assert r.realization() == "1" +def test_ensemble_component_alias_2(): + r = Ensemble(member=None) + assert r.member() is None + assert r.realisation() is None + assert r.realization() is None + + @pytest.mark.parametrize( "input_d,ref", [ @@ -35,6 +42,14 @@ def test_ensemble_component_alias_1(): ], ("5",), ), + ( + [ + {"member": None}, + {"realisation": None}, + {"realization": None}, + ], + (None,), + ), ], ) def test_ensemble_component_from_dict_ok(input_d, ref): diff --git a/tests/grib/test_grib_ensemble.py b/tests/grib/test_grib_ensemble.py index 54d8b4c75..35842187b 100644 --- a/tests/grib/test_grib_ensemble.py +++ b/tests/grib/test_grib_ensemble.py @@ -26,3 +26,14 @@ def test_grib_ensemble_1(fl_type): assert f.ensemble.member() == "1" assert f.ensemble.realization() == "1" assert f.ensemble.realisation() == "1" + + +@pytest.mark.parametrize("fl_type", FL_TYPES) +# @pytest.mark.parametrize("fl_type", ["file"]) +def test_grib_ensemble_2(fl_type): + ds, _ = load_grib_data("test.grib", fl_type) + f = ds[0] + + assert f.ensemble.member() is None + assert f.ensemble.realization() is None + assert f.ensemble.realisation() is None