From a07f2356f2a131b44662e8c9ed38325d991776f7 Mon Sep 17 00:00:00 2001 From: Bouwe Andela Date: Tue, 1 Oct 2024 11:20:14 +0200 Subject: [PATCH 1/4] Enable flake8-bugbear rules --- esmvalcore/_recipe/check.py | 12 +++++----- esmvalcore/_recipe/recipe.py | 12 ++++++---- esmvalcore/cmor/_fixes/fix.py | 4 ++-- esmvalcore/cmor/check.py | 2 +- esmvalcore/cmor/fix.py | 4 ++-- esmvalcore/config/_config_validators.py | 2 +- esmvalcore/config/_validated_config.py | 1 + esmvalcore/experimental/_warnings.py | 1 + esmvalcore/preprocessor/__init__.py | 4 ++-- esmvalcore/preprocessor/_derive/__init__.py | 2 +- esmvalcore/preprocessor/_derive/ctotal.py | 4 ++-- esmvalcore/preprocessor/_derive/ohc.py | 2 +- esmvalcore/preprocessor/_io.py | 2 +- esmvalcore/preprocessor/_mapping.py | 4 ++-- esmvalcore/preprocessor/_regrid.py | 4 ++-- esmvalcore/preprocessor/_regrid_esmpy.py | 10 ++++---- esmvalcore/preprocessor/_shared.py | 6 ++--- pyproject.toml | 5 ++++ .../integration/preprocessor/_io/test_load.py | 6 +++-- tests/integration/recipe/test_recipe.py | 6 ++--- tests/integration/test_task.py | 8 +++++-- .../experimental/test_run_recipe.py | 2 +- tests/unit/cmor/test_fix.py | 2 +- tests/unit/config/test_config_object.py | 6 +++-- tests/unit/preprocessor/_area/test_area.py | 2 +- .../_multimodel/test_multimodel.py | 6 +---- tests/unit/preprocessor/_other/test_other.py | 24 +++++++++---------- tests/unit/test_dataset.py | 2 +- tests/unit/test_iris_helpers.py | 8 +++---- 29 files changed, 83 insertions(+), 70 deletions(-) diff --git a/esmvalcore/_recipe/check.py b/esmvalcore/_recipe/check.py index 4e4fa6d2b9..d1dd7a89a2 100644 --- a/esmvalcore/_recipe/check.py +++ b/esmvalcore/_recipe/check.py @@ -44,12 +44,12 @@ def ncl_version(): try: cmd = [ncl, "-V"] version = subprocess.check_output(cmd, universal_newlines=True) - except subprocess.CalledProcessError: + except subprocess.CalledProcessError as exc: logger.error("Failed to execute '%s'", " ".join(" ".join(cmd))) raise RecipeError( "Recipe contains NCL scripts, but your NCL " "installation appears to be broken." - ) + ) from exc version = version.strip() logger.info("Found NCL version %s", version) @@ -565,7 +565,7 @@ def _check_regular_stat(step, step_settings): try: get_iris_aggregator(operator, **operator_kwargs) except ValueError as exc: - raise RecipeError(f"Invalid options for {step}: {exc}") + raise RecipeError(f"Invalid options for {step}: {exc}") from exc def _check_mm_stat(step, step_settings): @@ -575,11 +575,11 @@ def _check_mm_stat(step, step_settings): try: (operator, kwargs) = _get_operator_and_kwargs(stat) except ValueError as exc: - raise RecipeError(str(exc)) + raise RecipeError(str(exc)) from exc try: get_iris_aggregator(operator, **kwargs) except ValueError as exc: - raise RecipeError(f"Invalid options for {step}: {exc}") + raise RecipeError(f"Invalid options for {step}: {exc}") from exc def regridding_schemes(settings: dict): @@ -626,4 +626,4 @@ def regridding_schemes(settings: dict): f"https://docs.esmvaltool.org/projects/ESMValCore/en/latest" f"/recipe/preprocessor.html#generic-regridding-schemes for " f"details." - ) + ) from exc diff --git a/esmvalcore/_recipe/recipe.py b/esmvalcore/_recipe/recipe.py index 06bb2fd1a4..2ddf109db2 100644 --- a/esmvalcore/_recipe/recipe.py +++ b/esmvalcore/_recipe/recipe.py @@ -418,11 +418,11 @@ def _update_multiproduct(input_products, order, preproc_dir, step): called from the input products, the products that are created here need to be added to their ancestors products' settings (). """ - products = {p for p in input_products if step in p.settings} - if not products: + multiproducts = {p for p in input_products if step in p.settings} + if not multiproducts: return input_products, {} - settings = list(products)[0].settings[step] + settings = list(multiproducts)[0].settings[step] if step == "ensemble_statistics": check.ensemble_statistics_preproc(settings) @@ -431,14 +431,16 @@ def _update_multiproduct(input_products, order, preproc_dir, step): check.multimodel_statistics_preproc(settings) grouping = settings.get("groupby", None) - downstream_settings = _get_downstream_settings(step, order, products) + downstream_settings = _get_downstream_settings(step, order, multiproducts) relevant_settings = { "output_products": defaultdict(dict) } # pass to ancestors output_products = set() - for identifier, products in _group_products(products, by_key=grouping): + for identifier, products in _group_products( + multiproducts, by_key=grouping + ): common_attributes = _get_common_attributes(products, settings) statistics = settings.get("statistics", []) diff --git a/esmvalcore/cmor/_fixes/fix.py b/esmvalcore/cmor/_fixes/fix.py index 5aa41f6486..973ac57d0b 100644 --- a/esmvalcore/cmor/_fixes/fix.py +++ b/esmvalcore/cmor/_fixes/fix.py @@ -141,7 +141,7 @@ def get_cube_from_list( Raises ------ - Exception + ValueError No cube is found. Returns @@ -155,7 +155,7 @@ def get_cube_from_list( for cube in cubes: if cube.var_name == short_name: return cube - raise Exception(f'Cube for variable "{short_name}" not found') + raise ValueError(f'Cube for variable "{short_name}" not found') def fix_data(self, cube: Cube) -> Cube: """Apply fixes to the data of the cube. diff --git a/esmvalcore/cmor/check.py b/esmvalcore/cmor/check.py index 342d0fff66..4978932878 100644 --- a/esmvalcore/cmor/check.py +++ b/esmvalcore/cmor/check.py @@ -133,7 +133,7 @@ def __init__( "checks have been clearly separated in ESMValCore version " "2.10.0." ) - warnings.warn(msg, ESMValCoreDeprecationWarning) + warnings.warn(msg, ESMValCoreDeprecationWarning, stacklevel=2) # TODO: remove in v2.12 diff --git a/esmvalcore/cmor/fix.py b/esmvalcore/cmor/fix.py index 2e3209897d..173015e159 100644 --- a/esmvalcore/cmor/fix.py +++ b/esmvalcore/cmor/fix.py @@ -167,7 +167,7 @@ def fix_metadata( "longer perform CMOR checks. Fixes and CMOR checks have been " "clearly separated in ESMValCore version 2.10.0." ) - warnings.warn(msg, ESMValCoreDeprecationWarning) + warnings.warn(msg, ESMValCoreDeprecationWarning, stacklevel=2) # Update extra_facets with variable information given as regular arguments # to this function @@ -292,7 +292,7 @@ def fix_data( "longer perform CMOR checks. Fixes and CMOR checks have been " "clearly separated in ESMValCore version 2.10.0." ) - warnings.warn(msg, ESMValCoreDeprecationWarning) + warnings.warn(msg, ESMValCoreDeprecationWarning, stacklevel=2) # Update extra_facets with variable information given as regular arguments # to this function diff --git a/esmvalcore/config/_config_validators.py b/esmvalcore/config/_config_validators.py index 23034ce5c2..92f5666b06 100644 --- a/esmvalcore/config/_config_validators.py +++ b/esmvalcore/config/_config_validators.py @@ -362,7 +362,7 @@ def _handle_deprecation( f"been deprecated in ESMValCore version {deprecated_version} and is " f"scheduled for removal in version {remove_version}.{more_info}" ) - warnings.warn(deprecation_msg, ESMValCoreDeprecationWarning) + warnings.warn(deprecation_msg, ESMValCoreDeprecationWarning, stacklevel=2) # Example usage: see removed files in diff --git a/esmvalcore/config/_validated_config.py b/esmvalcore/config/_validated_config.py index 898abf3bb8..dca0a543f4 100644 --- a/esmvalcore/config/_validated_config.py +++ b/esmvalcore/config/_validated_config.py @@ -121,6 +121,7 @@ def check_missing(self): warnings.warn( f"`{key}` is not defined{more_info}", MissingConfigParameter, + stacklevel=1, ) def copy(self): diff --git a/esmvalcore/experimental/_warnings.py b/esmvalcore/experimental/_warnings.py index ddc474f568..548dc4e853 100644 --- a/esmvalcore/experimental/_warnings.py +++ b/esmvalcore/experimental/_warnings.py @@ -14,4 +14,5 @@ def _warning_formatter(message, category, filename, lineno, line=None): "\n Thank you for trying out the new ESMValCore API." "\n Note that this API is experimental and may be subject to change." "\n More info: https://github.com/ESMValGroup/ESMValCore/issues/498", + stacklevel=1, ) diff --git a/esmvalcore/preprocessor/__init__.py b/esmvalcore/preprocessor/__init__.py index 3429078a5d..851aae49f0 100644 --- a/esmvalcore/preprocessor/__init__.py +++ b/esmvalcore/preprocessor/__init__.py @@ -717,13 +717,13 @@ def _run(self, _): if step in product.settings: product.apply(step, self.debug) if block == blocks[-1]: - product.cubes # pylint: disable=pointless-statement + product.cubes # noqa: B018 pylint: disable=pointless-statement product.close() saved.add(product.filename) for product in self.products: if product.filename not in saved: - product.cubes # pylint: disable=pointless-statement + product.cubes # noqa: B018 pylint: disable=pointless-statement product.close() metadata_files = write_metadata( diff --git a/esmvalcore/preprocessor/_derive/__init__.py b/esmvalcore/preprocessor/_derive/__init__.py index 065845ef4d..add5d822e6 100644 --- a/esmvalcore/preprocessor/_derive/__init__.py +++ b/esmvalcore/preprocessor/_derive/__init__.py @@ -27,7 +27,7 @@ def _get_all_derived_variables(): module = importlib.import_module( f"esmvalcore.preprocessor._derive.{short_name}" ) - derivers[short_name] = getattr(module, "DerivedVariable") + derivers[short_name] = module.DerivedVariable return derivers diff --git a/esmvalcore/preprocessor/_derive/ctotal.py b/esmvalcore/preprocessor/_derive/ctotal.py index 8d8d00faef..159289f13e 100644 --- a/esmvalcore/preprocessor/_derive/ctotal.py +++ b/esmvalcore/preprocessor/_derive/ctotal.py @@ -37,12 +37,12 @@ def calculate(cubes): c_soil_cube = cubes.extract_cube( Constraint(name="soil_mass_content_of_carbon") ) - except iris.exceptions.ConstraintMismatchError: + except iris.exceptions.ConstraintMismatchError as exc: raise ValueError( f"No cube from {cubes} can be loaded with " f"standard name CMIP5: soil_carbon_content " f"or CMIP6: soil_mass_content_of_carbon" - ) + ) from exc c_veg_cube = cubes.extract_cube( Constraint(name="vegetation_carbon_content") ) diff --git a/esmvalcore/preprocessor/_derive/ohc.py b/esmvalcore/preprocessor/_derive/ohc.py index 05590c9f3b..6cea2b06f5 100644 --- a/esmvalcore/preprocessor/_derive/ohc.py +++ b/esmvalcore/preprocessor/_derive/ohc.py @@ -74,7 +74,7 @@ def calculate(cubes): contains_dimension=t_coord_dim, dim_coords=False ) ] - for coord, dims in dim_coords + aux_coords: + for coord, _ in dim_coords + aux_coords: cube.remove_coord(coord) new_cube = cube * volume new_cube *= RHO_CP diff --git a/esmvalcore/preprocessor/_io.py b/esmvalcore/preprocessor/_io.py index d30255ec13..5f83b1946c 100644 --- a/esmvalcore/preprocessor/_io.py +++ b/esmvalcore/preprocessor/_io.py @@ -321,7 +321,7 @@ def _sort_cubes_by_time(cubes): msg = "One or more cubes {} are missing".format( cubes ) + " time coordinate: {}".format(str(exc)) - raise ValueError(msg) + raise ValueError(msg) from exc except TypeError as error: msg = ( "Cubes cannot be sorted " diff --git a/esmvalcore/preprocessor/_mapping.py b/esmvalcore/preprocessor/_mapping.py index ccbeed2816..a84df1e67e 100644 --- a/esmvalcore/preprocessor/_mapping.py +++ b/esmvalcore/preprocessor/_mapping.py @@ -57,10 +57,10 @@ def ref_to_dims_index_as_index(cube, ref): """Get dim for index ref.""" try: dim = int(ref) - except (ValueError, TypeError): + except (ValueError, TypeError) as exc: raise ValueError( "{} Incompatible type {} for slicing".format(ref, type(ref)) - ) + ) from exc if dim < 0 or dim > cube.ndim: msg = ( "Requested an iterator over a dimension ({}) " diff --git a/esmvalcore/preprocessor/_regrid.py b/esmvalcore/preprocessor/_regrid.py index a8558f6ee1..2fc34e4d85 100644 --- a/esmvalcore/preprocessor/_regrid.py +++ b/esmvalcore/preprocessor/_regrid.py @@ -588,7 +588,7 @@ def _load_scheme(src_cube: Cube, tgt_cube: Cube, scheme: str | dict): "version 2.11.0, ESMValCore is able to determine the most " "suitable regridding scheme based on the input data." ) - warnings.warn(msg, ESMValCoreDeprecationWarning) + warnings.warn(msg, ESMValCoreDeprecationWarning, stacklevel=2) scheme = "nearest" if scheme == "linear_extrapolate": @@ -601,7 +601,7 @@ def _load_scheme(src_cube: Cube, tgt_cube: Cube, scheme: str | dict): "latest/recipe/preprocessor.html#generic-regridding-schemes)." "This is an exact replacement." ) - warnings.warn(msg, ESMValCoreDeprecationWarning) + warnings.warn(msg, ESMValCoreDeprecationWarning, stacklevel=2) scheme = "linear" loaded_scheme = Linear(extrapolation_mode="extrapolate") logger.debug("Loaded regridding scheme %s", loaded_scheme) diff --git a/esmvalcore/preprocessor/_regrid_esmpy.py b/esmvalcore/preprocessor/_regrid_esmpy.py index b2cb559406..e4d0b40ba6 100755 --- a/esmvalcore/preprocessor/_regrid_esmpy.py +++ b/esmvalcore/preprocessor/_regrid_esmpy.py @@ -8,7 +8,7 @@ try: import ESMF as esmpy # noqa: N811 except ImportError: - raise exc + raise exc from None import warnings import iris @@ -78,8 +78,8 @@ def __init__( ): """Initialize class instance.""" # These regridders are not lazy, so load source and target data once. - src_cube.data # pylint: disable=pointless-statement - tgt_cube.data # pylint: disable=pointless-statement + src_cube.data # # noqa: B018 pylint: disable=pointless-statement + tgt_cube.data # # noqa: B018 pylint: disable=pointless-statement self.src_cube = src_cube self.tgt_cube = tgt_cube self.method = method @@ -100,7 +100,7 @@ def __call__(self, cube: Cube) -> Cube: """ # These regridders are not lazy, so load source data once. - cube.data # pylint: disable=pointless-statement + cube.data # # noqa: B018 pylint: disable=pointless-statement src_rep, dst_rep = get_grid_representants(cube, self.tgt_cube) regridder = build_regridder( src_rep, dst_rep, self.method, mask_threshold=self.mask_threshold @@ -140,7 +140,7 @@ def __init__(self, mask_threshold: float = 0.99): "`esmvalcore.preprocessor.regrid_schemes.IrisESMFRegrid` " "instead." ) - warnings.warn(msg, ESMValCoreDeprecationWarning) + warnings.warn(msg, ESMValCoreDeprecationWarning, stacklevel=2) self.mask_threshold = mask_threshold def __repr__(self) -> str: diff --git a/esmvalcore/preprocessor/_shared.py b/esmvalcore/preprocessor/_shared.py index 04490bdda4..9bc62f9227 100644 --- a/esmvalcore/preprocessor/_shared.py +++ b/esmvalcore/preprocessor/_shared.py @@ -82,7 +82,7 @@ def get_iris_aggregator( f"scheduled for removal in version 2.12.0. Please use 'std_dev' " f"instead. This is an exact replacement." ) - warnings.warn(msg, ESMValCoreDeprecationWarning) + warnings.warn(msg, ESMValCoreDeprecationWarning, stacklevel=2) operator = "std_dev" cap_operator = "STD_DEV" elif re.match(r"^(P\d{1,2})(\.\d*)?$", cap_operator): @@ -94,7 +94,7 @@ def get_iris_aggregator( f"`percent=XX.YY` instead. Example: `percent=95.0` for 'p95.0'. " f"This is an exact replacement." ) - warnings.warn(msg, ESMValCoreDeprecationWarning) + warnings.warn(msg, ESMValCoreDeprecationWarning, stacklevel=2) aggregator_kwargs["percent"] = float(operator[1:]) operator = "percentile" cap_operator = "PERCENTILE" @@ -122,7 +122,7 @@ def get_iris_aggregator( except (ValueError, TypeError) as exc: raise ValueError( f"Invalid kwargs for operator '{operator}': {str(exc)}" - ) + ) from exc return (aggregator, aggregator_kwargs) diff --git a/pyproject.toml b/pyproject.toml index 5a45ca2ab9..5abbbaa1d1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -33,6 +33,7 @@ disable = [ line-length = 79 [tool.ruff.lint] select = [ + "B", "E", # pycodestyle "F", # pyflakes "I", # isort @@ -42,5 +43,9 @@ select = [ ignore = [ "E501", # Disable line-too-long as this is taken care of by the formatter. ] +[tool.ruff.lint.per-file-ignores] +"tests/**.py" = [ + "B011", # `assert False` is valid test code. +] [tool.ruff.lint.isort] known-first-party = ["esmvalcore"] diff --git a/tests/integration/preprocessor/_io/test_load.py b/tests/integration/preprocessor/_io/test_load.py index 1df7c3bb55..4c76ba2651 100644 --- a/tests/integration/preprocessor/_io/test_load.py +++ b/tests/integration/preprocessor/_io/test_load.py @@ -90,7 +90,7 @@ def test_callback_remove_attributes_from_coords(self): ) for coord in cube.coords(): for attr in attributes: - self.assertTrue(attr not in cube.attributes) + self.assertTrue(attr not in coord.attributes) def test_callback_fix_lat_units(self): """Test callback for fixing units.""" @@ -118,7 +118,9 @@ def test_fail_empty_cubes(self, mock_load_raw): def load_with_warning(*_, **__): """Mock load with a warning.""" warnings.warn( - "This is a custom expected warning", category=UserWarning + "This is a custom expected warning", + category=UserWarning, + stacklevel=2, ) return CubeList([Cube(0)]) diff --git a/tests/integration/recipe/test_recipe.py b/tests/integration/recipe/test_recipe.py index 75bed13e9f..50c17376f6 100644 --- a/tests/integration/recipe/test_recipe.py +++ b/tests/integration/recipe/test_recipe.py @@ -1750,7 +1750,7 @@ def test_extract_shape_raises( def _test_output_product_consistency(products, preprocessor, statistics): product_out = defaultdict(list) - for i, product in enumerate(products): + for product in products: settings = product.settings.get(preprocessor) if settings: output_products = settings["output_products"] @@ -1760,7 +1760,7 @@ def _test_output_product_consistency(products, preprocessor, statistics): product_out[identifier, statistic].append(preproc_file) # Make sure that output products are consistent - for (identifier, statistic), value in product_out.items(): + for (_, statistic), value in product_out.items(): assert statistic in statistics assert len(set(value)) == 1, "Output products are not equal" @@ -1908,7 +1908,7 @@ def test_multi_model_statistics_exclude(tmp_path, patched_datafinder, session): assert len(product_out) == len(statistics) assert "OBS" not in product_out - for id, prods in product_out: + for id, _ in product_out: assert id != "OBS" assert id == "CMIP5" task._initialize_product_provenance() diff --git a/tests/integration/test_task.py b/tests/integration/test_task.py index 2fb56b2cc4..cb8c632fbf 100644 --- a/tests/integration/test_task.py +++ b/tests/integration/test_task.py @@ -341,7 +341,9 @@ def _get_diagnostic_tasks(tmp_path, diagnostic_text, extension): def test_diagnostic_run_task(monkeypatch, executable, diag_text, tmp_path): """Run DiagnosticTask that will not fail.""" - def _run(self, input_filesi=[]): + def _run(self, input_filesi=None): + if input_filesi is None: + input_filesi = [] print(f"running task {self.name}") task = _get_diagnostic_tasks(tmp_path, diag_text, executable[1]) @@ -356,7 +358,9 @@ def test_diagnostic_run_task_fail( ): """Run DiagnosticTask that will fail.""" - def _run(self, input_filesi=[]): + def _run(self, input_filesi=None): + if input_filesi is None: + input_filesi = [] print(f"running task {self.name}") task = _get_diagnostic_tasks(tmp_path, diag_text[0], executable[1]) diff --git a/tests/sample_data/experimental/test_run_recipe.py b/tests/sample_data/experimental/test_run_recipe.py index 2abdd22197..2eae29a5b5 100644 --- a/tests/sample_data/experimental/test_run_recipe.py +++ b/tests/sample_data/experimental/test_run_recipe.py @@ -96,7 +96,7 @@ def test_run_recipe(monkeypatch, task, ssh, recipe, tmp_path, caplog): assert isinstance(output.read_main_log(), str) assert isinstance(output.read_main_log_debug(), str) - for task, task_output in output.items(): + for _, task_output in output.items(): assert isinstance(task_output, TaskOutput) assert len(task_output) > 0 diff --git a/tests/unit/cmor/test_fix.py b/tests/unit/cmor/test_fix.py index 01038d2786..02e250ca85 100644 --- a/tests/unit/cmor/test_fix.py +++ b/tests/unit/cmor/test_fix.py @@ -101,7 +101,7 @@ def test_get_second_cube(self): def test_get_default_raises(self): """Check that the default raises (Fix is not a cube).""" - with pytest.raises(Exception): + with pytest.raises(ValueError): self.fix.get_cube_from_list(self.cubes) def test_get_default(self): diff --git a/tests/unit/config/test_config_object.py b/tests/unit/config/test_config_object.py index ac301fb43e..54c0b1a0cf 100644 --- a/tests/unit/config/test_config_object.py +++ b/tests/unit/config/test_config_object.py @@ -18,9 +18,11 @@ def environment(**kwargs): """Temporary environment variables.""" backup = deepcopy(os.environ) - os.environ = kwargs + os.environ.clear() + os.environ.update(kwargs) yield - os.environ = backup + os.environ.clear() + os.environ.update(backup) def test_config_class(): diff --git a/tests/unit/preprocessor/_area/test_area.py b/tests/unit/preprocessor/_area/test_area.py index 9e88002aaa..99eaf3f150 100644 --- a/tests/unit/preprocessor/_area/test_area.py +++ b/tests/unit/preprocessor/_area/test_area.py @@ -1333,7 +1333,7 @@ def test_update_shapefile_path_abs(session, tmp_path): # Test with Path and str object for shapefile_in in (shapefile, str(shapefile)): - shapefile_out = _update_shapefile_path(shapefile, session=session) + shapefile_out = _update_shapefile_path(shapefile_in, session=session) assert isinstance(shapefile_out, Path) assert shapefile_out == shapefile diff --git a/tests/unit/preprocessor/_multimodel/test_multimodel.py b/tests/unit/preprocessor/_multimodel/test_multimodel.py index 5bc5513451..78872e56e9 100644 --- a/tests/unit/preprocessor/_multimodel/test_multimodel.py +++ b/tests/unit/preprocessor/_multimodel/test_multimodel.py @@ -195,11 +195,7 @@ def get_cubes_for_validation_test(frequency, lazy=False): def get_cube_for_equal_coords_test(num_cubes): """Set up cubes with equal auxiliary coordinates.""" - cubes = [] - - for num in range(num_cubes): - cube = generate_cube_from_dates("monthly") - cubes.append(cube) + cubes = [generate_cube_from_dates("monthly") for _ in range(num_cubes)] # Create cubes that have one exactly equal coordinate ('year'), one # coordinate with matching names ('m') and one coordinate with non-matching diff --git a/tests/unit/preprocessor/_other/test_other.py b/tests/unit/preprocessor/_other/test_other.py index a2237bfb6a..c50bed0a83 100644 --- a/tests/unit/preprocessor/_other/test_other.py +++ b/tests/unit/preprocessor/_other/test_other.py @@ -120,9 +120,9 @@ def test_histogram_defaults(cube, lazy): ) np.testing.assert_allclose(result.data.mask, [False] * 10) bin_coord = result.coord("air_temperature") - bin_coord.shape == (10,) - bin_coord.dtype == np.float64 - bin_coord.bounds_dtype == np.float64 + assert bin_coord.shape == (10,) + assert bin_coord.dtype == np.float64 + assert bin_coord.bounds_dtype == np.float64 np.testing.assert_allclose( bin_coord.points, [0.35, 1.05, 1.75, 2.45, 3.15, 3.85, 4.55, 5.25, 5.95, 6.65], @@ -196,9 +196,9 @@ def test_histogram_over_time(cube, lazy, weights, normalization): np.testing.assert_allclose(result.data, expected_data) np.testing.assert_allclose(result.data.mask, expected_data.mask) bin_coord = result.coord("air_temperature") - bin_coord.shape == (10,) - bin_coord.dtype == np.float64 - bin_coord.bounds_dtype == np.float64 + assert bin_coord.shape == (3,) + assert bin_coord.dtype == np.float64 + assert bin_coord.bounds_dtype == np.float64 np.testing.assert_allclose(bin_coord.points, [5.5, 7.5, 9.5]) np.testing.assert_allclose( bin_coord.bounds, @@ -231,9 +231,9 @@ def test_histogram_fully_masked(cube, lazy, normalization): ) np.testing.assert_equal(result.data.mask, [True] * 10) bin_coord = result.coord("air_temperature") - bin_coord.shape == (10,) - bin_coord.dtype == np.float64 - bin_coord.bounds_dtype == np.float64 + assert bin_coord.shape == (10,) + assert bin_coord.dtype == np.float64 + assert bin_coord.bounds_dtype == np.float64 np.testing.assert_allclose( bin_coord.points, [0.5, 1.5, 2.5, 3.5, 4.5, 5.5, 6.5, 7.5, 8.5, 9.5], @@ -303,9 +303,9 @@ def test_histogram_weights(cube, lazy, weights, normalization): np.testing.assert_allclose(result.data, expected_data) np.testing.assert_allclose(result.data.mask, expected_data.mask) bin_coord = result.coord("air_temperature") - bin_coord.shape == (10,) - bin_coord.dtype == np.float64 - bin_coord.bounds_dtype == np.float64 + assert bin_coord.shape == (3,) + assert bin_coord.dtype == np.float64 + assert bin_coord.bounds_dtype == np.float64 np.testing.assert_allclose(bin_coord.points, [1.0, 3.0, 6.0]) np.testing.assert_allclose( bin_coord.bounds, diff --git a/tests/unit/test_dataset.py b/tests/unit/test_dataset.py index 66d23306ec..406ce9f060 100644 --- a/tests/unit/test_dataset.py +++ b/tests/unit/test_dataset.py @@ -110,7 +110,7 @@ def test_session_setter(): assert ds._session is None assert ds.supplementaries[0]._session is None - ds.session + ds.session # noqa: B018 assert isinstance(ds.session, esmvalcore.config.Session) assert ds.session == ds.supplementaries[0].session diff --git a/tests/unit/test_iris_helpers.py b/tests/unit/test_iris_helpers.py index ccfd6fbbf6..1b5066ae51 100644 --- a/tests/unit/test_iris_helpers.py +++ b/tests/unit/test_iris_helpers.py @@ -329,10 +329,10 @@ def test_rechunk_cube_partly_lazy(cube_3d, complete_dims): input_cube = cube_3d.copy() # Realize some arrays - input_cube.data - input_cube.coord("xyz").points - input_cube.coord("xyz").bounds - input_cube.cell_measure("cell_measure").data + input_cube.data # noqa: B018 + input_cube.coord("xyz").points # noqa: B018 + input_cube.coord("xyz").bounds # noqa: B018 + input_cube.cell_measure("cell_measure").data # noqa: B018 result = rechunk_cube(input_cube, complete_dims, remaining_dims=2) From ce17001a92781f5335edac937548b7aa95ddc0a0 Mon Sep 17 00:00:00 2001 From: Bouwe Andela Date: Tue, 1 Oct 2024 11:24:36 +0200 Subject: [PATCH 2/4] Add documentation --- doc/contributing.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/contributing.rst b/doc/contributing.rst index ee47974e90..44e05ed7f9 100644 --- a/doc/contributing.rst +++ b/doc/contributing.rst @@ -602,7 +602,7 @@ that feature should be removed in version 2.7: "ESMValCore version 2.5 and is scheduled for removal in " "version 2.7. Add additional text (e.g., description of " "alternatives) here.") - warnings.warn(deprecation_msg, ESMValCoreDeprecationWarning) + warnings.warn(deprecation_msg, ESMValCoreDeprecationWarning, stacklevel=2) # Other code From 172d7360b310a21e40c96bf64d10091bf9505836 Mon Sep 17 00:00:00 2001 From: Bouwe Andela Date: Thu, 10 Oct 2024 21:11:50 +0200 Subject: [PATCH 3/4] Add tests for check.ncl_version --- esmvalcore/_recipe/check.py | 2 +- tests/integration/recipe/test_check.py | 75 ++++++++++++++++++++++++++ 2 files changed, 76 insertions(+), 1 deletion(-) diff --git a/esmvalcore/_recipe/check.py b/esmvalcore/_recipe/check.py index d1dd7a89a2..9f2e82e441 100644 --- a/esmvalcore/_recipe/check.py +++ b/esmvalcore/_recipe/check.py @@ -45,7 +45,7 @@ def ncl_version(): cmd = [ncl, "-V"] version = subprocess.check_output(cmd, universal_newlines=True) except subprocess.CalledProcessError as exc: - logger.error("Failed to execute '%s'", " ".join(" ".join(cmd))) + logger.error("Failed to execute '%s'", " ".join(cmd)) raise RecipeError( "Recipe contains NCL scripts, but your NCL " "installation appears to be broken." diff --git a/tests/integration/recipe/test_check.py b/tests/integration/recipe/test_check.py index b118162c15..42f23206da 100644 --- a/tests/integration/recipe/test_check.py +++ b/tests/integration/recipe/test_check.py @@ -1,6 +1,7 @@ """Integration tests for :mod:`esmvalcore._recipe.check`.""" import os.path +import subprocess from pathlib import Path from typing import Any, List from unittest import mock @@ -15,6 +16,80 @@ from esmvalcore.exceptions import RecipeError from esmvalcore.preprocessor import PreprocessorFile + +def test_ncl_version(mocker): + ncl = "/path/to/ncl" + mocker.patch.object( + check, + "which", + autospec=True, + return_value=ncl, + ) + mocker.patch.object( + check.subprocess, + "check_output", + autospec=True, + return_value="6.6.2\n", + ) + check.ncl_version() + + +def test_ncl_version_too_low(mocker): + ncl = "/path/to/ncl" + mocker.patch.object( + check, + "which", + autospec=True, + return_value=ncl, + ) + mocker.patch.object( + check.subprocess, + "check_output", + autospec=True, + return_value="6.3.2\n", + ) + with pytest.raises( + RecipeError, + match="NCL version 6.4 or higher is required", + ): + check.ncl_version() + + +def test_ncl_version_no_ncl(mocker): + mocker.patch.object( + check, + "which", + autospec=True, + return_value=None, + ) + with pytest.raises( + RecipeError, + match="cannot find an NCL installation", + ): + check.ncl_version() + + +def test_ncl_version_broken(mocker): + ncl = "/path/to/ncl" + mocker.patch.object( + check, + "which", + autospec=True, + return_value=ncl, + ) + mocker.patch.object( + check.subprocess, + "check_output", + autospec=True, + side_effect=subprocess.CalledProcessError(1, [ncl, "-V"]), + ) + with pytest.raises( + RecipeError, + match="NCL installation appears to be broken", + ): + check.ncl_version() + + ERR_ALL = "Looked for files matching%s" ERR_RANGE = "No input data available for years {} in files:\n{}" VAR = { From 4ea247af0f1fff62c833682c385f1ff353c44a2d Mon Sep 17 00:00:00 2001 From: Bouwe Andela Date: Tue, 15 Oct 2024 19:27:06 +0200 Subject: [PATCH 4/4] Update esmvalcore/config/_config_object.py Co-authored-by: Manuel Schlund <32543114+schlunma@users.noreply.github.com> --- esmvalcore/config/_config_object.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esmvalcore/config/_config_object.py b/esmvalcore/config/_config_object.py index 5c66170365..489e2301b2 100644 --- a/esmvalcore/config/_config_object.py +++ b/esmvalcore/config/_config_object.py @@ -92,7 +92,7 @@ def __init__(self, *args, **kwargs): "Do not instantiate `Config` objects directly, this will lead " "to unexpected behavior. Use `esmvalcore.config.CFG` instead." ) - warnings.warn(msg, UserWarning, stacklevel=1) + warnings.warn(msg, UserWarning, stacklevel=2) # TODO: remove in v2.14.0 @classmethod