From 82b41f10688d32b316d3b3c14d955c40c3091852 Mon Sep 17 00:00:00 2001 From: Stephen Moseley Date: Tue, 30 Sep 2025 09:40:52 +0100 Subject: [PATCH] Adds call_object_method. --- improver/api/__init__.py | 1 + improver/utilities/call_object_method.py | 31 ++++++++++ .../utilities/test_call_object_method.py | 57 +++++++++++++++++++ 3 files changed, 89 insertions(+) create mode 100644 improver/utilities/call_object_method.py create mode 100644 improver_tests/utilities/test_call_object_method.py diff --git a/improver/api/__init__.py b/improver/api/__init__.py index 53dd1ff5b8..96472449ee 100644 --- a/improver/api/__init__.py +++ b/improver/api/__init__.py @@ -30,6 +30,7 @@ "BaseNeighbourhoodProcessing": "improver.nbhood.nbhood", "CalculateForecastBias": "improver.calibration.simple_bias_correction", "CalibratedForecastDistributionParameters": "improver.calibration.emos_calibration", + "call_object_method": "improver.utilities.call_object_method", "ChooseDefaultWeightsLinear": "improver.blending.weights", "ChooseDefaultWeightsNonLinear": "improver.blending.weights", "ChooseDefaultWeightsTriangular": "improver.blending.weights", diff --git a/improver/utilities/call_object_method.py b/improver/utilities/call_object_method.py new file mode 100644 index 0000000000..a072cd73c9 --- /dev/null +++ b/improver/utilities/call_object_method.py @@ -0,0 +1,31 @@ +# (C) Crown Copyright, Met Office. All rights reserved. +# +# This file is part of 'IMPROVER' and is released under the BSD 3-Clause license. +# See LICENSE in the root of the repository for full licensing details. + +"""module to give access to callable methods on objects.""" + + +def call_object_method(obj: object, method_name: str, **kwargs): + """ + Calls a method on an object with the supplied arguments. + + This method allows us to construct a callable method for DAGRunner to execute + where the method to be called is on an object that comes from another plugin. + + e.g. cube.collapsed("height", iris.analysis.SUM) becomes + call_object_method(cube, "collapsed", coords="height", aggregator=iris.analysis.SUM) + + Args: + obj: + The object containing the method to be called. + method_name: + The name of the method to be called. + **kwargs: + The keyword arguments to be passed to the method. + + Returns: + The return value from the called method. + """ + method = getattr(obj, method_name) + return method(**kwargs) diff --git a/improver_tests/utilities/test_call_object_method.py b/improver_tests/utilities/test_call_object_method.py new file mode 100644 index 0000000000..012306e0a8 --- /dev/null +++ b/improver_tests/utilities/test_call_object_method.py @@ -0,0 +1,57 @@ +# (C) Crown Copyright, Met Office. All rights reserved. +# +# This file is part of 'IMPROVER' and is released under the BSD 3-Clause license. +# See LICENSE in the root of the repository for full licensing details. +"""Tests for the call_object_method utility function.""" + +import iris.analysis +import numpy as np +import pytest +from iris.cube import Cube + +from improver.synthetic_data.set_up_test_cubes import set_up_variable_cube +from improver.utilities.call_object_method import call_object_method + + +@pytest.fixture(name="cube") +def cube_fixture() -> Cube: + """Set up a cube of data""" + data = np.array([[[1, 2], [3, 4]], [[5, 6], [7, 8]]], dtype=np.float32) + cube = set_up_variable_cube( + data, + name="test_variable", + units="m/s", + vertical_levels=[1000, 2000], + height=True, + ) + return cube + + +def test_call_object_method(cube): + """Test that call_object_method correctly calls a method on an object.""" + result = call_object_method( + cube, "collapsed", coords="height", aggregator=iris.analysis.SUM + ) + expected_data = np.array([[6.0, 8.0], [10.0, 12.0]], dtype=np.float32) + assert np.array_equal(result.data, expected_data) + assert result.name() == "test_variable" + assert result.units == "m/s" + + +def test_call_object_method_invalid_method(cube): + """Test that call_object_method raises an AttributeError for an invalid method.""" + with pytest.raises(AttributeError): + call_object_method(cube, "non_existent_method") + + +def test_call_object_method_invalid_args(cube): + """Test that call_object_method raises a TypeError for invalid arguments.""" + with pytest.raises(TypeError): + call_object_method(cube, "collapsed", non_existent_arg=True) + + +def test_call_object_method_no_args(cube): + """Test that call_object_method works with no additional arguments.""" + result = call_object_method(cube, "copy") + assert result == cube + assert result is not cube # Ensure it's a copy, not the same object