diff --git a/docs/config.md b/docs/config.md index e41fdab..98df3a6 100644 --- a/docs/config.md +++ b/docs/config.md @@ -45,14 +45,10 @@ And as Python module: ```python def export_config(): return [ - {"files": ["**/*.zarr", "**/*.nc"]}, - { - "plugins": { - "xcube": "xrlint.plugins.xcube" - } - }, - "recommended", - "xcube/recommended" + {"files": ["**/*.zarr", "**/*.nc"]}, + {"plugins": {"xcube": "xrlint.plugins.xcube"}}, + "recommended", + "xcube/recommended", ] ``` diff --git a/docs/mkruleref.py b/docs/mkruleref.py index 40bc3db..359e6fc 100644 --- a/docs/mkruleref.py +++ b/docs/mkruleref.py @@ -2,9 +2,9 @@ # This software is distributed under the terms and conditions of the # MIT license (https://mit-license.org/). +from xrlint.config import plugins_from_entry_points from xrlint.plugin import Plugin from xrlint.rule import RuleConfig -from xrlint.config import plugins_from_entry_points # for icons, see # https://squidfunk.github.io/mkdocs-material/reference/icons-emojis/ diff --git a/examples/rule_testing.py b/examples/rule_testing.py index e59bbb2..6af8028 100644 --- a/examples/rule_testing.py +++ b/examples/rule_testing.py @@ -12,7 +12,6 @@ from xrlint.rule import RuleContext, RuleOp, define_rule from xrlint.testing import RuleTest, RuleTester - # ---------------------------------------------------- # Place the rule implementation code in its own module # ---------------------------------------------------- @@ -42,8 +41,8 @@ def validate_dataset(self, ctx: RuleContext, node: DatasetNode): tester = RuleTester() -valid_dataset = xr.Dataset(attrs=dict(title="Hello World!")) -invalid_dataset = xr.Dataset(attrs=dict(title="Hello Hamburg!")) +valid_dataset = xr.Dataset(attrs={"title": "Hello World!"}) +invalid_dataset = xr.Dataset(attrs={"title": "Hello Hamburg!"}) # You can use the tester to run a test directly # diff --git a/notebooks/mkdataset.py b/notebooks/mkdataset.py index 63e8b51..4322198 100644 --- a/notebooks/mkdataset.py +++ b/notebooks/mkdataset.py @@ -18,15 +18,15 @@ def make_dataset() -> xr.Dataset: """Create a dataset that passes xrlint core rules.""" return xr.Dataset( - attrs=dict( - Conventions="CF-1.10", - title="SST-Climatology Subset", - history="2025-01-31 17:31:00 - created;", - institution="BC", - source="SST CCI L4", - references="https://climate.esa.int/en/projects/sea-surface-temperature/", - comment="Demo dataset", - ), + attrs={ + "Conventions": "CF-1.10", + "title": "SST-Climatology Subset", + "history": "2025-01-31 17:31:00 - created;", + "institution": "BC", + "source": "SST CCI L4", + "references": "https://climate.esa.int/en/projects/sea-surface-temperature/", + "comment": "Demo dataset", + }, coords={ "x": xr.DataArray( np.linspace(-180, 180, nx), diff --git a/tests/cli/test_config.py b/tests/cli/test_config.py index c40dab1..160943b 100644 --- a/tests/cli/test_config.py +++ b/tests/cli/test_config.py @@ -101,36 +101,42 @@ def test_read_config_invalid_arg(self): read_config(None) def test_read_config_json_with_format_error(self): - with text_file("config.json", "{") as config_path: - with pytest.raises( + with ( + text_file("config.json", "{") as config_path, + pytest.raises( ConfigError, match=( "config.json:" " Expecting property name enclosed in double quotes:" " line 1 column 2 \\(char 1\\)" ), - ): - read_config(config_path) + ), + ): + read_config(config_path) def test_read_config_yaml_with_format_error(self): - with text_file("config.yaml", "}") as config_path: - with pytest.raises( + with ( + text_file("config.yaml", "}") as config_path, + pytest.raises( ConfigError, match="config.yaml: while parsing a block node", - ): - read_config(config_path) + ), + ): + read_config(config_path) def test_read_config_yaml_with_type_error(self): - with text_file("config.yaml", "97") as config_path: - with pytest.raises( + with ( + text_file("config.yaml", "97") as config_path, + pytest.raises( ConfigError, match=( r"config\.yaml\: config must be of type" r" Config \| ConfigObjectLike \| str \| Sequence\[ConfigObjectLike \| str\]," r" but got int" ), - ): - read_config(config_path) + ), + ): + read_config(config_path) def test_read_config_with_unknown_format(self): with pytest.raises( @@ -141,38 +147,45 @@ def test_read_config_with_unknown_format(self): def test_read_config_py_no_export(self): py_code = "x = 42\n" - with text_file(self.new_config_py(), py_code) as config_path: - with pytest.raises( + with ( + text_file(self.new_config_py(), py_code) as config_path, + pytest.raises( ConfigError, match=( "config_1002.py: attribute 'export_config'" " not found in module 'config_1002'" ), - ): - read_config(config_path) + ), + ): + read_config(config_path) def test_read_config_py_with_value_error(self): py_code = "def export_config():\n raise ValueError('value is useless!')\n" - with text_file(self.new_config_py(), py_code) as config_path: - with pytest.raises( + with ( + text_file(self.new_config_py(), py_code) as config_path, + pytest.raises( ValueError, match="value is useless!", - ): - read_config(config_path) + ), + ): + read_config(config_path) def test_read_config_py_with_os_error(self): py_code = "def export_config():\n raise OSError('where is my hat?')\n" - with text_file(self.new_config_py(), py_code) as config_path: - with pytest.raises( + with ( + text_file(self.new_config_py(), py_code) as config_path, + pytest.raises( ConfigError, match="where is my hat?", - ): - read_config(config_path) + ), + ): + read_config(config_path) def test_read_config_py_with_invalid_config_list(self): py_code = "def export_config():\n return 42\n" - with text_file(self.new_config_py(), py_code) as config_path: - with pytest.raises( + with ( + text_file(self.new_config_py(), py_code) as config_path, + pytest.raises( ConfigError, match=( r"\.py: failed converting value of 'config_1003:export_config':" @@ -180,8 +193,9 @@ def test_read_config_py_with_invalid_config_list(self): r" Config \| ConfigObjectLike \| str \| Sequence\[ConfigObjectLike \| str\]," r" but got int" ), - ): - read_config(config_path) + ), + ): + read_config(config_path) class CliConfigResolveTest(unittest.TestCase): diff --git a/tests/cli/test_main.py b/tests/cli/test_main.py index 814de68..0c73222 100644 --- a/tests/cli/test_main.py +++ b/tests/cli/test_main.py @@ -26,20 +26,20 @@ # noinspection PyTypeChecker class CliMainTest(TestCase): - files = ["dataset1.zarr", "dataset1.nc", "dataset2.zarr", "dataset2.nc"] + files = ["dataset1.zarr", "dataset1.nc", "dataset2.zarr", "dataset2.nc"] # noqa: RUF012 ok_config_yaml = "- rules:\n var-units: error\n" fail_config_yaml = "- rules:\n conventions: error\n" # noinspection SpellCheckingInspection invalid_config_yaml = "- recommentet\n" - datasets = dict( - dataset1=xr.Dataset(attrs={"title": "Test 1"}), - dataset2=xr.Dataset( + datasets = { # noqa: RUF012 + "dataset1": xr.Dataset(attrs={"title": "Test 1"}), + "dataset2": xr.Dataset( attrs={"title": "Test 2"}, data_vars={"v": xr.DataArray([1, 2, 3], attrs={"units": "m/s"})}, ), - ) + } temp_dir: str last_cwd: str diff --git a/tests/formatters/test_simple.py b/tests/formatters/test_simple.py index 1bf6d15..b22be13 100644 --- a/tests/formatters/test_simple.py +++ b/tests/formatters/test_simple.py @@ -11,7 +11,7 @@ class SimpleTest(TestCase): - errors_and_warnings = [ + errors_and_warnings = [ # noqa: RUF012 Result( file_path="test1.nc", config_object=ConfigObject(), @@ -23,7 +23,7 @@ class SimpleTest(TestCase): ) ] - warnings_only = [ + warnings_only = [ # noqa: RUF012 Result( file_path="test2.nc", config_object=ConfigObject(), diff --git a/tests/plugins/acdd/rules/test_attributes.py b/tests/plugins/acdd/rules/test_attributes.py index 4373f37..7b043ff 100644 --- a/tests/plugins/acdd/rules/test_attributes.py +++ b/tests/plugins/acdd/rules/test_attributes.py @@ -1,9 +1,9 @@ import xarray as xr -from xrlint.testing import RuleTest, RuleTester from xrlint.plugins.acdd.rules.attributes import ( Attributes_1_3_Highly_Recommended, ) +from xrlint.testing import RuleTest, RuleTester valid_1_3_highly_rec_dataset = xr.Dataset( attrs={ diff --git a/tests/plugins/acdd/rules/test_conventions.py b/tests/plugins/acdd/rules/test_conventions.py index 7856bc0..a931db8 100644 --- a/tests/plugins/acdd/rules/test_conventions.py +++ b/tests/plugins/acdd/rules/test_conventions.py @@ -1,7 +1,7 @@ import xarray as xr -from xrlint.testing import RuleTest, RuleTester from xrlint.plugins.acdd.rules.conventions import Conventions +from xrlint.testing import RuleTest, RuleTester valid_dataset_0 = xr.Dataset(attrs={"Conventions": "ACDD-1.3"}) diff --git a/tests/plugins/acdd/rules/test_id_blanks.py b/tests/plugins/acdd/rules/test_id_blanks.py index b733a1c..41b1fd3 100644 --- a/tests/plugins/acdd/rules/test_id_blanks.py +++ b/tests/plugins/acdd/rules/test_id_blanks.py @@ -1,7 +1,7 @@ import xarray as xr -from xrlint.testing import RuleTest, RuleTester from xrlint.plugins.acdd.rules.no_id_blanks import NoBlanksInID +from xrlint.testing import RuleTest, RuleTester valid_dataset_0 = xr.Dataset(attrs={"id": "testing_dataset"}) diff --git a/tests/plugins/acdd/rules/test_iso_dates.py b/tests/plugins/acdd/rules/test_iso_dates.py index 577e417..3f8f497 100644 --- a/tests/plugins/acdd/rules/test_iso_dates.py +++ b/tests/plugins/acdd/rules/test_iso_dates.py @@ -1,7 +1,7 @@ import xarray as xr -from xrlint.testing import RuleTest, RuleTester from xrlint.plugins.acdd.rules.iso_dates import IsoDates +from xrlint.testing import RuleTest, RuleTester valid_dataset_0 = xr.Dataset(attrs={"date_created": "2023-10-05T12:34:56Z"}) valid_dataset_1 = xr.Dataset(attrs={"date_modified": "2023-10-05"}) diff --git a/tests/plugins/acdd/rules/test_metadata_link.py b/tests/plugins/acdd/rules/test_metadata_link.py index 16c73ae..5e17fc5 100644 --- a/tests/plugins/acdd/rules/test_metadata_link.py +++ b/tests/plugins/acdd/rules/test_metadata_link.py @@ -1,7 +1,7 @@ import xarray as xr -from xrlint.testing import RuleTest, RuleTester from xrlint.plugins.acdd.rules.metadata_link import MetadataLink +from xrlint.testing import RuleTest, RuleTester valid_dataset_0 = xr.Dataset(attrs={"metadata_link": "http://example.com/metadata"}) valid_dataset_1 = xr.Dataset(attrs={"metadata_link": "https://example.com/metadata"}) diff --git a/tests/plugins/core/rules/test_content_desc.py b/tests/plugins/core/rules/test_content_desc.py index f0d2119..75aed54 100644 --- a/tests/plugins/core/rules/test_content_desc.py +++ b/tests/plugins/core/rules/test_content_desc.py @@ -7,73 +7,73 @@ from xrlint.plugins.core.rules.content_desc import ContentDesc from xrlint.testing import RuleTest, RuleTester -global_attrs = dict( - title="OC-Climatology", - history="2025-01-26: created", -) +global_attrs = { + "title": "OC-Climatology", + "history": "2025-01-26: created", +} -common_attrs = dict( - institution="ESA", - source="a.nc; b.nc", - references="!", - comment="?", -) +common_attrs = { + "institution": "ESA", + "source": "a.nc; b.nc", + "references": "!", + "comment": "?", +} all_attrs = global_attrs | common_attrs time_coord = xr.DataArray( - [1, 2, 3], dims="time", attrs=dict(units="days since 2025-01-01") + [1, 2, 3], dims="time", attrs={"units": "days since 2025-01-01"} ) valid_dataset_0 = xr.Dataset( attrs=all_attrs, - data_vars=dict(chl=xr.DataArray([1, 2, 3], dims="time", attrs=dict())), - coords=dict(time=time_coord), + data_vars={"chl": xr.DataArray([1, 2, 3], dims="time", attrs={})}, + coords={"time": time_coord}, ) valid_dataset_1 = xr.Dataset( attrs=global_attrs, - data_vars=dict(chl=xr.DataArray([1, 2, 3], dims="time", attrs=common_attrs)), - coords=dict(time=time_coord), + data_vars={"chl": xr.DataArray([1, 2, 3], dims="time", attrs=common_attrs)}, + coords={"time": time_coord}, ) valid_dataset_1a = xr.Dataset( attrs=global_attrs, - data_vars=dict( - chl=xr.DataArray([1, 2, 3], dims="time", attrs=common_attrs), - crs=xr.DataArray(0, attrs=dict(grid_mapping_name="...")), - ), - coords=dict(time=time_coord), + data_vars={ + "chl": xr.DataArray([1, 2, 3], dims="time", attrs=common_attrs), + "crs": xr.DataArray(0, attrs={"grid_mapping_name": "..."}), + }, + coords={"time": time_coord}, ) valid_dataset_1b = xr.Dataset( attrs=global_attrs, - data_vars=dict( - chl=xr.DataArray([1, 2, 3], dims="time", attrs=common_attrs), - chl_unc=xr.DataArray(0, attrs=dict(units="...")), - ), - coords=dict(time=time_coord), + data_vars={ + "chl": xr.DataArray([1, 2, 3], dims="time", attrs=common_attrs), + "chl_unc": xr.DataArray(0, attrs={"units": "..."}), + }, + coords={"time": time_coord}, ) valid_dataset_2 = xr.Dataset( attrs=global_attrs, - data_vars=dict(chl=xr.DataArray([1, 2, 3], dims="time", attrs=dict())), - coords=dict(time=time_coord), + data_vars={"chl": xr.DataArray([1, 2, 3], dims="time", attrs={})}, + coords={"time": time_coord}, ) valid_dataset_3 = xr.Dataset( attrs=global_attrs, - data_vars=dict( - chl=xr.DataArray([1, 2, 3], dims="time", attrs=dict(description="Bla!")) - ), - coords=dict(time=time_coord), + data_vars={ + "chl": xr.DataArray([1, 2, 3], dims="time", attrs={"description": "Bla!"}) + }, + coords={"time": time_coord}, ) invalid_dataset_0 = xr.Dataset() invalid_dataset_1 = xr.Dataset( - attrs=dict(), - data_vars=dict(chl=xr.DataArray([1, 2, 3], dims="time", attrs=dict())), - coords=dict(time=time_coord), + attrs={}, + data_vars={"chl": xr.DataArray([1, 2, 3], dims="time", attrs={})}, + coords={"time": time_coord}, ) invalid_dataset_2 = xr.Dataset( attrs=global_attrs, - data_vars=dict(chl=xr.DataArray([1, 2, 3], dims="time", attrs=dict())), - coords=dict(time=time_coord), + data_vars={"chl": xr.DataArray([1, 2, 3], dims="time", attrs={})}, + coords={"time": time_coord}, ) ContentDescTest = RuleTester.define_test( diff --git a/tests/plugins/core/rules/test_conventions.py b/tests/plugins/core/rules/test_conventions.py index 8472eef..3605b9e 100644 --- a/tests/plugins/core/rules/test_conventions.py +++ b/tests/plugins/core/rules/test_conventions.py @@ -7,11 +7,11 @@ from xrlint.plugins.core.rules.conventions import Conventions from xrlint.testing import RuleTest, RuleTester -valid_dataset_0 = xr.Dataset(attrs=dict(Conventions="CF-1.10")) +valid_dataset_0 = xr.Dataset(attrs={"Conventions": "CF-1.10"}) invalid_dataset_0 = xr.Dataset() -invalid_dataset_1 = xr.Dataset(attrs=dict(Conventions=1.12)) -invalid_dataset_2 = xr.Dataset(attrs=dict(Conventions="CF 1.10")) +invalid_dataset_1 = xr.Dataset(attrs={"Conventions": 1.12}) +invalid_dataset_2 = xr.Dataset(attrs={"Conventions": "CF 1.10"}) ConventionsTest = RuleTester.define_test( diff --git a/tests/plugins/core/rules/test_coords_for_dims.py b/tests/plugins/core/rules/test_coords_for_dims.py index 306efba..d8322bb 100644 --- a/tests/plugins/core/rules/test_coords_for_dims.py +++ b/tests/plugins/core/rules/test_coords_for_dims.py @@ -7,9 +7,9 @@ from xrlint.plugins.core.rules.coords_for_dims import CoordsForDims from xrlint.testing import RuleTest, RuleTester -valid_dataset_1 = xr.Dataset(attrs=dict(title="empty")) +valid_dataset_1 = xr.Dataset(attrs={"title": "empty"}) valid_dataset_2 = xr.Dataset( - attrs=dict(title="v-data"), + attrs={"title": "v-data"}, coords={"x": xr.DataArray([0, 0.1, 0.2], dims="x", attrs={"units": "s"})}, data_vars={"v": xr.DataArray([10, 20, 30], dims="x", attrs={"units": "m/s"})}, ) diff --git a/tests/plugins/core/rules/test_grid_mappings.py b/tests/plugins/core/rules/test_grid_mappings.py index 13252ab..b825e82 100644 --- a/tests/plugins/core/rules/test_grid_mappings.py +++ b/tests/plugins/core/rules/test_grid_mappings.py @@ -11,7 +11,7 @@ def make_dataset(): return xr.Dataset( - attrs=dict(title="OC Data"), + attrs={"title": "OC Data"}, coords={ "x": xr.DataArray(np.linspace(0, 1, 4), dims="x", attrs={"units": "m"}), "y": xr.DataArray(np.linspace(0, 1, 3), dims="y", attrs={"units": "m"}), @@ -40,7 +40,7 @@ def make_dataset(): ) -valid_dataset_1 = xr.Dataset(attrs=dict(title="Empty")) +valid_dataset_1 = xr.Dataset(attrs={"title": "Empty"}) valid_dataset_2 = make_dataset() invalid_dataset_1 = make_dataset().drop_vars("crs") diff --git a/tests/plugins/core/rules/test_no_empty_attrs.py b/tests/plugins/core/rules/test_no_empty_attrs.py index 6ea92b0..a67fc0e 100644 --- a/tests/plugins/core/rules/test_no_empty_attrs.py +++ b/tests/plugins/core/rules/test_no_empty_attrs.py @@ -7,9 +7,9 @@ from xrlint.plugins.core.rules.no_empty_attrs import NoEmptyAttrs from xrlint.testing import RuleTest, RuleTester -valid_dataset_1 = xr.Dataset(attrs=dict(title="empty")) +valid_dataset_1 = xr.Dataset(attrs={"title": "empty"}) valid_dataset_2 = xr.Dataset( - attrs=dict(title="v-data"), + attrs={"title": "v-data"}, coords={"x": xr.DataArray([0, 0.1, 0.2], dims="x", attrs={"units": "s"})}, data_vars={"v": xr.DataArray([10, 20, 30], dims="x", attrs={"units": "m/s"})}, ) diff --git a/tests/plugins/core/rules/test_no_empty_chunks.py b/tests/plugins/core/rules/test_no_empty_chunks.py index 49a98e9..f6d6f77 100644 --- a/tests/plugins/core/rules/test_no_empty_chunks.py +++ b/tests/plugins/core/rules/test_no_empty_chunks.py @@ -8,7 +8,7 @@ from xrlint.testing import RuleTest, RuleTester # valid, because it is not chunked -valid_dataset_0 = xr.Dataset(attrs=dict(title="OC-Climatology")) +valid_dataset_0 = xr.Dataset(attrs={"title": "OC-Climatology"}) valid_dataset_0.encoding["source"] = "test.zarr" valid_dataset_0["sst"] = xr.DataArray([273, 274, 272], dims="time") valid_dataset_0["sst"].encoding["_FillValue"] = 0 diff --git a/tests/plugins/core/rules/test_var_desc.py b/tests/plugins/core/rules/test_var_desc.py index 69234de..9780a40 100644 --- a/tests/plugins/core/rules/test_var_desc.py +++ b/tests/plugins/core/rules/test_var_desc.py @@ -7,62 +7,62 @@ from xrlint.plugins.core.rules.var_desc import VarDesc from xrlint.testing import RuleTest, RuleTester -pressure_attrs = dict( - long_name="mean sea level pressure", - units="hPa", - standard_name="air_pressure_at_sea_level", -) +pressure_attrs = { + "long_name": "mean sea level pressure", + "units": "hPa", + "standard_name": "air_pressure_at_sea_level", +} time_coord = xr.DataArray( - [1, 2, 3], dims="time", attrs=dict(units="days since 2025-01-01") + [1, 2, 3], dims="time", attrs={"units": "days since 2025-01-01"} ) valid_dataset_0 = xr.Dataset( - coords=dict(time=time_coord), + coords={"time": time_coord}, ) valid_dataset_1 = xr.Dataset( - data_vars=dict(pressure=xr.DataArray([1, 2, 3], dims="time", attrs=pressure_attrs)), - coords=dict(time=time_coord), + data_vars={"pressure": xr.DataArray([1, 2, 3], dims="time", attrs=pressure_attrs)}, + coords={"time": time_coord}, ) valid_dataset_2 = xr.Dataset( - data_vars=dict( - chl=xr.DataArray( - [1, 2, 3], dims="time", attrs=dict(description="It is air pressure") + data_vars={ + "chl": xr.DataArray( + [1, 2, 3], dims="time", attrs={"description": "It is air pressure"} ) - ), - coords=dict(time=time_coord), + }, + coords={"time": time_coord}, ) invalid_dataset_0 = xr.Dataset( - attrs=dict(), - data_vars=dict(chl=xr.DataArray([1, 2, 3], dims="time", attrs=dict())), - coords=dict(time=time_coord), + attrs={}, + data_vars={"chl": xr.DataArray([1, 2, 3], dims="time", attrs={})}, + coords={"time": time_coord}, ) invalid_dataset_1 = xr.Dataset( - attrs=dict(), - data_vars=dict( - chl=xr.DataArray( + attrs={}, + data_vars={ + "chl": xr.DataArray( [1, 2, 3], dims="time", - attrs=dict(standard_name="air_pressure_at_sea_level"), + attrs={"standard_name": "air_pressure_at_sea_level"}, ) - ), - coords=dict(time=time_coord), + }, + coords={"time": time_coord}, ) invalid_dataset_2 = xr.Dataset( - attrs=dict(), - data_vars=dict( - chl=xr.DataArray( - [1, 2, 3], dims="time", attrs=dict(long_name="mean sea level pressure") + attrs={}, + data_vars={ + "chl": xr.DataArray( + [1, 2, 3], dims="time", attrs={"long_name": "mean sea level pressure"} ) - ), - coords=dict(time=time_coord), + }, + coords={"time": time_coord}, ) invalid_dataset_3 = xr.Dataset( - attrs=dict(), - data_vars=dict(chl=xr.DataArray([1, 2, 3], dims="time", attrs=pressure_attrs)), - coords=dict(time=time_coord), + attrs={}, + data_vars={"chl": xr.DataArray([1, 2, 3], dims="time", attrs=pressure_attrs)}, + coords={"time": time_coord}, ) VarDescTest = RuleTester.define_test( diff --git a/tests/plugins/core/rules/test_var_flags.py b/tests/plugins/core/rules/test_var_flags.py index a3b12cc..5525f54 100644 --- a/tests/plugins/core/rules/test_var_flags.py +++ b/tests/plugins/core/rules/test_var_flags.py @@ -10,26 +10,26 @@ valid_dataset_0 = xr.Dataset() valid_dataset_1 = xr.Dataset( - attrs=dict(title="sensor-data"), + attrs={"title": "sensor-data"}, data_vars={ "sensor_status_qc": xr.DataArray( [1, 3, 5, 2, 0, 5], dims="x", - attrs=dict( - long_name="Sensor Status", - standard_name="status_flag", - _FillValue=0, - valid_range=[1, 15], - flag_masks=[1, 2, 12, 12, 12], - flag_values=[1, 2, 4, 8, 12], - flag_meanings=( + attrs={ + "long_name": "Sensor Status", + "standard_name": "status_flag", + "_FillValue": 0, + "valid_range": [1, 15], + "flag_masks": [1, 2, 12, 12, 12], + "flag_values": [1, 2, 4, 8, 12], + "flag_meanings": ( "low_battery" " hardware_fault" " offline_mode" " calibration_mode" " maintenance_mode" ), - ), + }, ) }, ) diff --git a/tests/plugins/core/rules/test_var_missing_data.py b/tests/plugins/core/rules/test_var_missing_data.py index 0099efc..87b206d 100644 --- a/tests/plugins/core/rules/test_var_missing_data.py +++ b/tests/plugins/core/rules/test_var_missing_data.py @@ -9,7 +9,7 @@ valid_dataset_0 = xr.Dataset() valid_dataset_1 = xr.Dataset( - attrs=dict(title="v-data"), + attrs={"title": "v-data"}, coords={"t": xr.DataArray([0, 1, 2], dims="t", attrs={"units": "seconds"})}, data_vars={"v": xr.DataArray([10, 20, 30], dims="t", attrs={"units": "m/s"})}, ) diff --git a/tests/plugins/core/rules/test_var_units.py b/tests/plugins/core/rules/test_var_units.py index bf00bdb..13fb6cb 100644 --- a/tests/plugins/core/rules/test_var_units.py +++ b/tests/plugins/core/rules/test_var_units.py @@ -9,7 +9,7 @@ valid_dataset_0 = xr.Dataset() valid_dataset_1 = xr.Dataset( - attrs=dict(title="v-data"), + attrs={"title": "v-data"}, coords={"t": xr.DataArray([0, 1, 2], dims="t", attrs={"units": "seconds"})}, data_vars={"v": xr.DataArray([10, 20, 30], dims="t", attrs={"units": "m/s"})}, ) diff --git a/tests/plugins/xcube/helpers.py b/tests/plugins/xcube/helpers.py index a817058..c7e5c47 100644 --- a/tests/plugins/xcube/helpers.py +++ b/tests/plugins/xcube/helpers.py @@ -41,61 +41,61 @@ def make_cube(nx: int, ny: int, nt: int | None = None) -> xr.Dataset: an in-memory dataset with one 3-d data variable "chl" with dimensions ["time",] "lat", "lon". """ - x_attrs = dict( - long_name="longitude", - standard_name="longitude", - units="degrees_east", - ) - y_attrs = dict( - long_name="latitude", - standard_name="latitude", - units="degrees_north", - ) + x_attrs = { + "long_name": "longitude", + "standard_name": "longitude", + "units": "degrees_east", + } + y_attrs = { + "long_name": "latitude", + "standard_name": "latitude", + "units": "degrees_north", + } dx = 180.0 / nx dy = 90.0 / ny x_data = np.linspace(-180 + dx, 180 - dx, nx) y_data = np.linspace(-90 + dy, 90 - dy, ny) - chl_attrs = dict( - long_name="chlorophyll concentration", - standard_name="chlorophyll_concentration", - units="mg/m^3", - _FillValue=0, - ) - chl_chunks = dict(lat=min(ny, 90), lon=min(nx, 90)) + chl_attrs = { + "long_name": "chlorophyll concentration", + "standard_name": "chlorophyll_concentration", + "units": "mg/m^3", + "_FillValue": 0, + } + chl_chunks = {"lat": min(ny, 90), "lon": min(nx, 90)} - ds_attrs = dict(title="Chlorophyll") + ds_attrs = {"title": "Chlorophyll"} - coords = dict( - lon=xr.DataArray(x_data, dims="lon", attrs=x_attrs), - lat=xr.DataArray(y_data, dims="lat", attrs=y_attrs), - ) + coords = { + "lon": xr.DataArray(x_data, dims="lon", attrs=x_attrs), + "lat": xr.DataArray(y_data, dims="lat", attrs=y_attrs), + } if nt is None: return xr.Dataset( - data_vars=dict( - chl=xr.DataArray( + data_vars={ + "chl": xr.DataArray( np.zeros((ny, nx)), dims=["lat", "lon"], attrs=chl_attrs ).chunk(**chl_chunks), - ), + }, coords=coords, attrs=ds_attrs, ) else: - time_attrs = dict( - long_name="time", - standard_name="time", - units="days since 2024-06-10:12:00:00 utc", - calendar="gregorian", - ) + time_attrs = { + "long_name": "time", + "standard_name": "time", + "units": "days since 2024-06-10:12:00:00 utc", + "calendar": "gregorian", + } coords.update(time=xr.DataArray(range(nt), dims="time", attrs=time_attrs)) return xr.Dataset( - data_vars=dict( - chl=xr.DataArray( + data_vars={ + "chl": xr.DataArray( np.zeros((nt, ny, nx)), dims=["time", "lat", "lon"], attrs=chl_attrs ).chunk(time=1, **chl_chunks), - ), + }, coords=coords, attrs=ds_attrs, ) diff --git a/tests/plugins/xcube/processors/test_mldataset.py b/tests/plugins/xcube/processors/test_mldataset.py index 825bef4..9b306e8 100644 --- a/tests/plugins/xcube/processors/test_mldataset.py +++ b/tests/plugins/xcube/processors/test_mldataset.py @@ -27,7 +27,7 @@ class MultiLevelDatasetProcessorTest(TestCase): num_levels = 4 meta_path = f"{levels_dir}/.zlevels" - meta_content = { + meta_content = { # noqa: RUF012 "version": "1.0", "num_levels": num_levels, "use_saved_levels": False, diff --git a/tests/plugins/xcube/rules/test_cube_dims_order.py b/tests/plugins/xcube/rules/test_cube_dims_order.py index c3b466a..62637a9 100644 --- a/tests/plugins/xcube/rules/test_cube_dims_order.py +++ b/tests/plugins/xcube/rules/test_cube_dims_order.py @@ -12,7 +12,7 @@ def make_dataset(dims: tuple[str, str, str]): n = 3 return xr.Dataset( - attrs=dict(title="v-data"), + attrs={"title": "v-data"}, coords={ "x": xr.DataArray(np.linspace(0, 1, n), dims="x", attrs={"units": "m"}), "y": xr.DataArray(np.linspace(0, 1, n), dims="y", attrs={"units": "m"}), diff --git a/tests/plugins/xcube/rules/test_data_var_colors.py b/tests/plugins/xcube/rules/test_data_var_colors.py index 8c84551..9f3bee6 100644 --- a/tests/plugins/xcube/rules/test_data_var_colors.py +++ b/tests/plugins/xcube/rules/test_data_var_colors.py @@ -13,7 +13,7 @@ def make_dataset(): dims = ["time", "y", "x"] n = 5 return xr.Dataset( - attrs=dict(title="v-data"), + attrs={"title": "v-data"}, coords={ dims[2]: xr.DataArray( np.linspace(0, 1, n), dims=dims[2], attrs={"units": "m"} diff --git a/tests/plugins/xcube/rules/test_dataset_title.py b/tests/plugins/xcube/rules/test_dataset_title.py index 3f2f68d..6f61211 100644 --- a/tests/plugins/xcube/rules/test_dataset_title.py +++ b/tests/plugins/xcube/rules/test_dataset_title.py @@ -7,11 +7,11 @@ from xrlint.plugins.xcube.rules.dataset_title import DatasetTitle from xrlint.testing import RuleTest, RuleTester -valid_dataset_0 = xr.Dataset(attrs=dict(title="OC-Climatology")) -valid_dataset_1 = xr.Dataset(attrs=dict(title="SST-Climatology")) +valid_dataset_0 = xr.Dataset(attrs={"title": "OC-Climatology"}) +valid_dataset_1 = xr.Dataset(attrs={"title": "SST-Climatology"}) invalid_dataset_0 = xr.Dataset() -invalid_dataset_1 = xr.Dataset(attrs=dict(title="")) +invalid_dataset_1 = xr.Dataset(attrs={"title": ""}) DatasetTitleTest = RuleTester.define_test( diff --git a/tests/plugins/xcube/rules/test_grid_mapping_naming.py b/tests/plugins/xcube/rules/test_grid_mapping_naming.py index 39ca479..686fcd0 100644 --- a/tests/plugins/xcube/rules/test_grid_mapping_naming.py +++ b/tests/plugins/xcube/rules/test_grid_mapping_naming.py @@ -11,7 +11,7 @@ def make_dataset(): return xr.Dataset( - attrs=dict(title="OC Data"), + attrs={"title": "OC Data"}, coords={ "x": xr.DataArray(np.linspace(0, 1, 4), dims="x", attrs={"units": "m"}), "y": xr.DataArray(np.linspace(0, 1, 3), dims="y", attrs={"units": "m"}), diff --git a/tests/plugins/xcube/rules/test_increasing_time.py b/tests/plugins/xcube/rules/test_increasing_time.py index 4965af7..d81ae5e 100644 --- a/tests/plugins/xcube/rules/test_increasing_time.py +++ b/tests/plugins/xcube/rules/test_increasing_time.py @@ -13,7 +13,7 @@ def make_dataset(): dims = ["time", "y", "x"] n = 5 return xr.Dataset( - attrs=dict(title="v-data"), + attrs={"title": "v-data"}, coords={ dims[2]: xr.DataArray( np.linspace(0, 1, n), dims=dims[2], attrs={"units": "m"} diff --git a/tests/plugins/xcube/rules/test_lat_lon_naming.py b/tests/plugins/xcube/rules/test_lat_lon_naming.py index e37b6c2..90d016e 100644 --- a/tests/plugins/xcube/rules/test_lat_lon_naming.py +++ b/tests/plugins/xcube/rules/test_lat_lon_naming.py @@ -13,7 +13,7 @@ def make_dataset(lat_dim: str, lon_dim: str): dims = ["time", lat_dim, lon_dim] n = 3 return xr.Dataset( - attrs=dict(title="v-data"), + attrs={"title": "v-data"}, coords={ lon_dim: xr.DataArray( np.linspace(0, 1, n), dims=lon_dim, attrs={"units": "m"} diff --git a/tests/plugins/xcube/rules/test_no_chunked_coords.py b/tests/plugins/xcube/rules/test_no_chunked_coords.py index eca9124..defb272 100644 --- a/tests/plugins/xcube/rules/test_no_chunked_coords.py +++ b/tests/plugins/xcube/rules/test_no_chunked_coords.py @@ -8,7 +8,7 @@ from xrlint.plugins.xcube.rules.no_chunked_coords import NoChunkedCoords from xrlint.testing import RuleTest, RuleTester -valid_dataset_0 = xr.Dataset(attrs=dict(title="Empty")) +valid_dataset_0 = xr.Dataset(attrs={"title": "Empty"}) valid_dataset_1 = make_cube(360, 180, 3) valid_dataset_2 = make_cube(90, 45, 20) # ok, below default limit 5: ceil(20 / 5) = 4 diff --git a/tests/plugins/xcube/rules/test_single_grid_mapping.py b/tests/plugins/xcube/rules/test_single_grid_mapping.py index 3ed5240..7966d1a 100644 --- a/tests/plugins/xcube/rules/test_single_grid_mapping.py +++ b/tests/plugins/xcube/rules/test_single_grid_mapping.py @@ -11,7 +11,7 @@ def make_dataset(): return xr.Dataset( - attrs=dict(title="OC Data"), + attrs={"title": "OC Data"}, coords={ "x": xr.DataArray(np.linspace(0, 1, 4), dims="x", attrs={"units": "m"}), "y": xr.DataArray(np.linspace(0, 1, 3), dims="y", attrs={"units": "m"}), @@ -40,7 +40,7 @@ def make_dataset(): ) -valid_dataset_1 = xr.Dataset(attrs=dict(title="Empty")) +valid_dataset_1 = xr.Dataset(attrs={"title": "Empty"}) valid_dataset_2 = make_dataset() valid_dataset_3 = make_dataset().rename({"crs": "spatial_ref"}) valid_dataset_4 = make_dataset().drop_vars("crs") diff --git a/tests/plugins/xcube/rules/test_time_naming.py b/tests/plugins/xcube/rules/test_time_naming.py index 5b61840..04031a3 100644 --- a/tests/plugins/xcube/rules/test_time_naming.py +++ b/tests/plugins/xcube/rules/test_time_naming.py @@ -14,7 +14,7 @@ def make_dataset(time_var: str, time_dim: str | None = None): dims = [time_dim, "y", "x"] n = 3 return xr.Dataset( - attrs=dict(title="v-data"), + attrs={"title": "v-data"}, coords={ "x": xr.DataArray(np.linspace(0, 1, n), dims="x", attrs={"units": "m"}), "y": xr.DataArray(np.linspace(0, 1, n), dims="y", attrs={"units": "m"}), diff --git a/tests/plugins/xcube/test_plugin.py b/tests/plugins/xcube/test_plugin.py index 6492078..8dbc7cc 100644 --- a/tests/plugins/xcube/test_plugin.py +++ b/tests/plugins/xcube/test_plugin.py @@ -38,7 +38,7 @@ def test_configs_complete(self): }, set(plugin.configs.keys()), ) - all_rule_names = set(f"xcube/{k}" for k in plugin.rules.keys()) + all_rule_names = {f"xcube/{k}" for k in plugin.rules} self.assertEqual( all_rule_names, set(plugin.configs["all"][-1].rules.keys()), diff --git a/tests/plugins/xcube/test_util.py b/tests/plugins/xcube/test_util.py index 2cb4e00..6611765 100644 --- a/tests/plugins/xcube/test_util.py +++ b/tests/plugins/xcube/test_util.py @@ -4,8 +4,7 @@ from unittest import TestCase -from xrlint.plugins.xcube.util import is_absolute_path -from xrlint.plugins.xcube.util import resolve_path +from xrlint.plugins.xcube.util import is_absolute_path, resolve_path class UtilTest(TestCase): diff --git a/tests/test_all.py b/tests/test_all.py index 1d5c197..55e7ca6 100644 --- a/tests/test_all.py +++ b/tests/test_all.py @@ -11,9 +11,9 @@ def test_api_is_complete(self): from xrlint.all import __all__ # noinspection PyUnresolvedReferences - keys = set( + keys = { k for k, v in xrl.__dict__.items() if isinstance(k, str) and not k.startswith("_") - ) + } self.assertEqual(set(__all__), keys) diff --git a/tests/test_config.py b/tests/test_config.py index 6ee4359..bedf4c9 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -107,9 +107,9 @@ def postprocess( processor = define_processor("myproc", op_class=MyProc) config_obj = ConfigObject( - plugins=dict( - myplugin=new_plugin("myplugin", processors=dict(myproc=processor)) - ) + plugins={ + "myplugin": new_plugin("myplugin", processors={"myproc": processor}) + } ) processor_op = config_obj.get_processor_op(MyProc()) diff --git a/tests/test_operation.py b/tests/test_operation.py index 86ef42c..c369b80 100644 --- a/tests/test_operation.py +++ b/tests/test_operation.py @@ -4,7 +4,6 @@ from abc import ABC from dataclasses import dataclass -from typing import Type from unittest import TestCase import pytest @@ -25,14 +24,14 @@ class ThingMeta(OperationMeta): @dataclass(kw_only=True, frozen=True) class Thing(Operation): meta: ThingMeta - op_class: Type[ThingOp] + op_class: type[ThingOp] @classmethod - def meta_class(cls) -> Type: + def meta_class(cls) -> type: return ThingMeta @classmethod - def op_base_class(cls) -> Type[ThingOp]: + def op_base_class(cls) -> type[ThingOp]: return ThingOp @classmethod @@ -40,7 +39,7 @@ def value_name(cls) -> str: return "thing" @classmethod - def define(cls, op_class: Type[ThingOp] | None = None, **kwargs): + def define(cls, op_class: type[ThingOp] | None = None, **kwargs): return cls.define_operation(op_class, **kwargs) @@ -162,7 +161,7 @@ def test_define_op(self): class MyThingOp3(ThingOp): """This is my 3rd thing.""" - value = Thing.define_operation(MyThingOp3, meta_kwargs=dict(version="1.0")) + value = Thing.define_operation(MyThingOp3, meta_kwargs={"version": "1.0"}) self.assertIsInstance(value, Thing) self.assertIsInstance(value.meta, ThingMeta) self.assertEqual("my-thing-op-3", value.meta.name) diff --git a/tests/test_testing.py b/tests/test_testing.py index 070748f..91fa803 100644 --- a/tests/test_testing.py +++ b/tests/test_testing.py @@ -19,10 +19,10 @@ def validate_dataset(self, ctx: RuleContext, node: DatasetNode): ctx.report("Datasets must have a title") -VALID_DATASET_1 = xr.Dataset(attrs=dict(title="OC-Climatology")) -VALID_DATASET_2 = xr.Dataset(attrs=dict(title="SST-Climatology")) +VALID_DATASET_1 = xr.Dataset(attrs={"title": "OC-Climatology"}) +VALID_DATASET_2 = xr.Dataset(attrs={"title": "SST-Climatology"}) INVALID_DATASET_1 = xr.Dataset() -INVALID_DATASET_2 = xr.Dataset(attrs=dict(title="")) +INVALID_DATASET_2 = xr.Dataset(attrs={"title": ""}) # noinspection PyMethodMayBeStatic diff --git a/tests/util/test_attrs.py b/tests/util/test_attrs.py index 950f251..337c777 100644 --- a/tests/util/test_attrs.py +++ b/tests/util/test_attrs.py @@ -6,7 +6,7 @@ import xarray as xr -from xrlint.node import DataTreeNode, DatasetNode +from xrlint.node import DatasetNode, DataTreeNode from xrlint.util.attrs import hierarchical_attrs diff --git a/tests/util/test_constructible.py b/tests/util/test_constructible.py index 41443e9..1caad85 100644 --- a/tests/util/test_constructible.py +++ b/tests/util/test_constructible.py @@ -2,14 +2,13 @@ # This software is distributed under the terms and conditions of the # MIT license (https://mit-license.org/). +from collections.abc import Mapping from dataclasses import dataclass, field from types import NoneType, UnionType from typing import ( TYPE_CHECKING, Any, Literal, - Mapping, - Optional, TypeAlias, Union, get_args, @@ -84,7 +83,7 @@ class UnresolvedTypesContainer(ComplexTypesContainer, SimpleTypesContainer): plugins: dict[str, "Plugin"] = field(default_factory=dict) @classmethod - def forward_refs(cls) -> Optional[Mapping[str, type]]: + def forward_refs(cls) -> Mapping[str, type] | None: from xrlint.plugin import Plugin from xrlint.rule import RuleConfig @@ -94,18 +93,15 @@ def forward_refs(cls) -> Optional[Mapping[str, type]]: } -T1: TypeAlias = int | str | Union[bool, None] | None -T2: TypeAlias = Optional[int] -T3: TypeAlias = Optional[Any] +T1: TypeAlias = int | str | bool | None +T2: TypeAlias = int | None +T3: TypeAlias = Any | None class TypingTest(TestCase): def test_assumptions(self): # self.assertTrue(isinstance(Any, type)) self.assertTrue(isinstance(UnionType, type)) - self.assertTrue(not isinstance(Union, type)) - self.assertTrue(not isinstance(Union, UnionType)) - self.assertTrue(Union != UnionType) self.assertEqual(None, get_origin("NoTypesContainer")) self.assertEqual(None, get_origin("dict")) @@ -114,13 +110,14 @@ def test_assumptions(self): (str, "NoTypesContainer"), get_args(dict[str, "NoTypesContainer"]) ) - self.assertEqual(Union, get_origin(T1)) + self.assertEqual(UnionType, get_origin(T1)) self.assertEqual({bool, int, str, NoneType}, set(get_args(T1))) - self.assertEqual(Union, get_origin(T2)) + self.assertEqual(UnionType, get_origin(T2)) self.assertEqual({int, NoneType}, set(get_args(T2))) - self.assertEqual(Union, get_origin(T3)) + # Python 3.10 may report Any | None as typing.Union. + self.assertIn(get_origin(T3), (Union, UnionType)) self.assertEqual({Any, NoneType}, set(get_args(T3))) @@ -208,7 +205,7 @@ def test_required_props_fail(self): RequiredPropsContainer.from_value({"x": 12.0, "z": 34.0}, "rpc") def test_no_types_ok(self): - ntc = NoTypesContainer.from_value(dict(u=True, v=654, w="abc")) + ntc = NoTypesContainer.from_value({"u": True, "v": 654, "w": "abc"}) self.assertEqual(True, ntc.u) self.assertEqual(654, ntc.v) self.assertEqual("abc", ntc.w) @@ -216,7 +213,15 @@ def test_no_types_ok(self): class MappingConstructibleTest(TestCase): def test_simple_ok(self): - kwargs = dict(a="?", b=True, c=12, d=34.56, e="uvw", f=bytes, g="on") + kwargs = { + "a": "?", + "b": True, + "c": 12, + "d": 34.56, + "e": "uvw", + "f": bytes, + "g": "on", + } container = SimpleTypesContainer(**kwargs) self.assertEqual(container, SimpleTypesContainer.from_value(kwargs)) self.assertIs(container, SimpleTypesContainer.from_value(container)) diff --git a/tests/util/test_filepattern.py b/tests/util/test_filepattern.py index 800ece7..61e5006 100644 --- a/tests/util/test_filepattern.py +++ b/tests/util/test_filepattern.py @@ -12,7 +12,6 @@ def test_basics(self): matcher = FilePattern("**/*.h5") self.assertEqual("**/*.h5", str(matcher)) self.assertEqual("FilePattern('**/*.h5')", repr(matcher)) - self.assertTrue(matcher == matcher) self.assertFalse(matcher == 5) self.assertTrue(matcher == FilePattern("**/*.h5")) self.assertFalse(matcher == FilePattern("**/*.nc")) diff --git a/tests/util/test_serializable.py b/tests/util/test_serializable.py index ec950b4..f785dbb 100644 --- a/tests/util/test_serializable.py +++ b/tests/util/test_serializable.py @@ -34,14 +34,14 @@ def __init__( class PlainComplexTypesContainer(JsonSerializable): def __init__( self, - p: PlainSimpleTypesContainer = PlainSimpleTypesContainer(), - q: dict[str, bool] = None, - r: dict[str, PlainSimpleTypesContainer] = None, - s: list[int] = None, - t: list[PlainSimpleTypesContainer] = None, - u: int | float | None = None, + p: PlainSimpleTypesContainer | None = None, + q: dict[str, bool] | None = None, + r: dict[str, PlainSimpleTypesContainer] | None = None, + s: list[int] | None = None, + t: list[PlainSimpleTypesContainer] | None = None, + u: float | None = None, ): - self.p = p + self.p = p if p is not None else PlainSimpleTypesContainer() self.q = q or {} self.r = r or {} self.s = s or [] @@ -104,8 +104,8 @@ def test_plain_simple_ok(self): def test_plain_complex_ok(self): container = PlainComplexTypesContainer( - q=dict(p=True, q=False), - r=dict(u=PlainSimpleTypesContainer(), v=PlainSimpleTypesContainer()), + q={"p": True, "q": False}, + r={"u": PlainSimpleTypesContainer(), "v": PlainSimpleTypesContainer()}, s=[1, 2, 3], t=[ PlainSimpleTypesContainer(c=5, d=6.7), @@ -192,10 +192,11 @@ def test_dataclass_simple_ok(self): def test_dataclass_complex_ok(self): container = DataclassComplexTypesContainer( - q=dict(p=True, q=False), - r=dict( - u=DataclassSimpleTypesContainer(), v=DataclassSimpleTypesContainer() - ), + q={"p": True, "q": False}, + r={ + "u": DataclassSimpleTypesContainer(), + "v": DataclassSimpleTypesContainer(), + }, s=[1, 2, 3], t=[ DataclassSimpleTypesContainer(c=5, d=6.7), diff --git a/xrlint/_linter/rulectx.py b/xrlint/_linter/rulectx.py index 87c0bb6..b62018e 100644 --- a/xrlint/_linter/rulectx.py +++ b/xrlint/_linter/rulectx.py @@ -101,7 +101,7 @@ def report( @contextlib.contextmanager def use_state(self, **new_state): - old_state = {k: getattr(self, k) for k in new_state.keys()} + old_state = {k: getattr(self, k) for k in new_state} try: for k, v in new_state.items(): setattr(self, k, v) diff --git a/xrlint/_linter/validate.py b/xrlint/_linter/validate.py index bf81399..efbfdef 100644 --- a/xrlint/_linter/validate.py +++ b/xrlint/_linter/validate.py @@ -85,7 +85,7 @@ def _open_dataset( ) -> tuple[xr.Dataset | xr.DataTree, float]: """Open a dataset.""" engine = opener_options.pop("engine", None) - if engine is None and (file_path.endswith(".zarr") or file_path.endswith(".zarr/")): + if engine is None and (file_path.endswith((".zarr", ".zarr/"))): engine = "zarr" try: t0 = time.time() diff --git a/xrlint/all.py b/xrlint/all.py index 4009c16..fcba6cc 100644 --- a/xrlint/all.py +++ b/xrlint/all.py @@ -35,43 +35,43 @@ from xrlint.version import version __all__ = [ - "XRLint", + "AttrNode", + "AttrsNode", "Config", "ConfigLike", "ConfigObject", "ConfigObjectLike", - "Linter", - "new_linter", + "DatasetNode", "EditInfo", - "Message", - "Result", - "Suggestion", - "get_rules_meta_for_results", "Formatter", "FormatterContext", "FormatterMeta", "FormatterOp", "FormatterRegistry", - "AttrNode", - "AttrsNode", - "VariableNode", - "DatasetNode", + "Linter", + "Message", "Node", "Plugin", "PluginMeta", - "new_plugin", "Processor", "ProcessorMeta", "ProcessorOp", - "define_processor", + "Result", "Rule", "RuleConfig", "RuleContext", "RuleExit", "RuleMeta", "RuleOp", - "define_rule", "RuleTest", "RuleTester", + "Suggestion", + "VariableNode", + "XRLint", + "define_processor", + "define_rule", + "get_rules_meta_for_results", + "new_linter", + "new_plugin", "version", ] diff --git a/xrlint/cli/config.py b/xrlint/cli/config.py index 1a9342c..2816929 100644 --- a/xrlint/cli/config.py +++ b/xrlint/cli/config.py @@ -48,7 +48,7 @@ def read_config(config_path: str | Path | PathLike[str]) -> Config: def _read_config_like(config_path: str) -> Any: - if config_path.endswith(".yml") or config_path.endswith(".yaml"): + if config_path.endswith((".yml", ".yaml")): return _read_config_yaml(config_path) if config_path.endswith(".json"): return _read_config_json(config_path) diff --git a/xrlint/cli/constants.py b/xrlint/cli/constants.py index a8ccba6..8ed38b0 100644 --- a/xrlint/cli/constants.py +++ b/xrlint/cli/constants.py @@ -4,7 +4,6 @@ from typing import Final - _MODULE_BASENAME: Final = "xrlint_config" _REGULAR_BASENAME: Final = "xrlint-config" diff --git a/xrlint/cli/engine.py b/xrlint/cli/engine.py index 63da2a8..0c60fa5 100644 --- a/xrlint/cli/engine.py +++ b/xrlint/cli/engine.py @@ -234,7 +234,7 @@ def format_results(self, results: Iterable[Result]) -> str: raise click.ClickException( f"unknown format {output_format!r}." f" The available formats are" - f" {', '.join(repr(k) for k in formatters.keys())}." + f" {', '.join(repr(k) for k in formatters)}." ) # Here we could pass and validate format-specific args/kwargs # against formatter.meta.schema diff --git a/xrlint/cli/main.py b/xrlint/cli/main.py index 7a381ca..daf476f 100644 --- a/xrlint/cli/main.py +++ b/xrlint/cli/main.py @@ -9,9 +9,9 @@ # Warning: do not import heavy stuff here, it can # slow down commands like "xrlint --help" otherwise. from xrlint.cli.constants import ( + DEFAULT_CONFIG_FILE_YAML, DEFAULT_MAX_WARNINGS, DEFAULT_OUTPUT_FORMAT, - DEFAULT_CONFIG_FILE_YAML, ) from xrlint.version import version diff --git a/xrlint/config.py b/xrlint/config.py index c85276a..f04ec56 100644 --- a/xrlint/config.py +++ b/xrlint/config.py @@ -223,7 +223,7 @@ def get_processor_op( raise ValueError(f"unknown processor {processor_spec!r}") return processor.op_class() - def merge(self, config: "ConfigObject", name: str = None) -> "ConfigObject": + def merge(self, config: "ConfigObject", name: str | None = None) -> "ConfigObject": return ConfigObject( name=name, files=self._merge_pattern_lists(self.files, config.files), diff --git a/xrlint/constants.py b/xrlint/constants.py index 1043cea..d2b12a1 100644 --- a/xrlint/constants.py +++ b/xrlint/constants.py @@ -28,4 +28,4 @@ SEVERITY_ENUM: Final[dict[int | str, int]] = ( SEVERITY_NAME_TO_CODE | SEVERITY_CODE_TO_CODE ) -SEVERITY_ENUM_TEXT: Final = ", ".join(f"{k!r}" for k in SEVERITY_ENUM.keys()) +SEVERITY_ENUM_TEXT: Final = ", ".join(f"{k!r}" for k in SEVERITY_ENUM) diff --git a/xrlint/formatter.py b/xrlint/formatter.py index 1519bd3..f59cef1 100644 --- a/xrlint/formatter.py +++ b/xrlint/formatter.py @@ -3,9 +3,9 @@ # MIT license (https://mit-license.org/). from abc import ABC, abstractmethod -from collections.abc import Iterable, Mapping +from collections.abc import Callable, Iterable, Mapping from dataclasses import dataclass -from typing import Any, Callable, Type +from typing import Any from xrlint.operation import Operation, OperationMeta from xrlint.result import Result, ResultStats @@ -72,15 +72,15 @@ class Formatter(Operation): meta: FormatterMeta """The formatter metadata.""" - op_class: Type[FormatterOp] + op_class: type[FormatterOp] """The class that implements the format operation.""" @classmethod - def meta_class(cls) -> Type: + def meta_class(cls) -> type: return FormatterMeta @classmethod - def op_base_class(cls) -> Type: + def op_base_class(cls) -> type: return FormatterOp @classmethod @@ -97,12 +97,12 @@ def define_formatter( name: str | None = None, version: str | None = None, schema: dict[str, Any] | list[dict[str, Any]] | bool | None = None, - ) -> Callable[[FormatterOp], Type[FormatterOp]] | Formatter: + ) -> Callable[[FormatterOp], type[FormatterOp]] | Formatter: """Decorator function.""" return Formatter.define_operation( None, registry=self._registrations, - meta_kwargs=dict(name=name, version=version, schema=schema), + meta_kwargs={"name": name, "version": version, "schema": schema}, ) def __getitem__(self, key: str) -> Formatter: diff --git a/xrlint/formatters/html.py b/xrlint/formatters/html.py index aaec003..9039d1f 100644 --- a/xrlint/formatters/html.py +++ b/xrlint/formatters/html.py @@ -18,9 +18,9 @@ version="1.0.0", schema=schema( "object", - properties=dict( - with_meta=schema("boolean", default=False), - ), + properties={ + "with_meta": schema("boolean", default=False), + }, ), ) class Html(FormatterOp): diff --git a/xrlint/formatters/json.py b/xrlint/formatters/json.py index ce10a66..5df3de1 100644 --- a/xrlint/formatters/json.py +++ b/xrlint/formatters/json.py @@ -16,10 +16,10 @@ version="1.0.0", schema=schema( "object", - properties=dict( - indent=schema("integer", minimum=0, maximum=8, default=2), - with_meta=schema("boolean", default=False), - ), + properties={ + "indent": schema("integer", minimum=0, maximum=8, default=2), + "with_meta": schema("boolean", default=False), + }, ), ) class Json(FormatterOp): diff --git a/xrlint/formatters/simple.py b/xrlint/formatters/simple.py index 5516e1e..bd0df95 100644 --- a/xrlint/formatters/simple.py +++ b/xrlint/formatters/simple.py @@ -19,10 +19,10 @@ version="1.0.0", schema=schema( "object", - properties=dict( - styled=schema("boolean", default=True), - output=schema("boolean", default=True), - ), + properties={ + "styled": schema("boolean", default=True), + "output": schema("boolean", default=True), + }, ), ) class Simple(FormatterOp): diff --git a/xrlint/node.py b/xrlint/node.py index 9b190a4..4774a5d 100644 --- a/xrlint/node.py +++ b/xrlint/node.py @@ -3,8 +3,9 @@ # MIT license (https://mit-license.org/). from abc import ABC +from collections.abc import Hashable from dataclasses import dataclass -from typing import Any, Hashable, Union +from typing import Any, Union import xarray as xr diff --git a/xrlint/operation.py b/xrlint/operation.py index b87c004..f05dfb1 100644 --- a/xrlint/operation.py +++ b/xrlint/operation.py @@ -2,10 +2,12 @@ # This software is distributed under the terms and conditions of the # MIT license (https://mit-license.org/). -from collections.abc import MutableMapping +from __future__ import annotations + +from collections.abc import Callable, MutableMapping from dataclasses import dataclass from inspect import getdoc, isclass -from typing import Any, Callable, Type +from typing import Any from xrlint.util.constructible import MappingConstructible from xrlint.util.importutil import import_value @@ -82,7 +84,7 @@ def to_json(self, value_name: str | None = None) -> str: return super().to_json(value_name=value_name) @classmethod - def _from_class(cls, value: Type, value_name: str) -> "Operation": + def _from_class(cls, value: type, value_name: str) -> Operation: # noinspection PyTypeChecker if issubclass(value, cls.op_base_class()): op_class = value @@ -102,7 +104,7 @@ def _from_class(cls, value: Type, value_name: str) -> "Operation": return super()._from_class(value, value_name) @classmethod - def _from_str(cls, value: str, value_name: str) -> "Operation": + def _from_str(cls, value: str, value_name: str) -> Operation: # noinspection PyTypeChecker operator, operator_ref = import_value( value, @@ -121,14 +123,14 @@ def op_import_attr_name(cls) -> str: return f"export_{cls.value_name()}" @classmethod - def meta_class(cls) -> Type: + def meta_class(cls) -> type: """Get the class of the instances of the `meta` field. Defaults to [OperationMeta][xrlint.operation.OperationMeta]. """ return OperationMeta @classmethod - def op_base_class(cls) -> Type: + def op_base_class(cls) -> type: """Get the base class from which all instances of the `op_class` must derive from. """ @@ -148,16 +150,16 @@ def value_type_name(cls) -> str: @classmethod def define_operation( cls, - op_class: Type | None, + op_class: type | None, *, - registry: MutableMapping[str, "Operation"] | None = None, + registry: MutableMapping[str, Operation] | None = None, meta_kwargs: dict[str, Any] | None = None, **kwargs, - ) -> Callable[[Type], Type] | "Operation": + ) -> Callable[[type], type] | Operation: """Defines an operation.""" meta_kwargs = meta_kwargs or {} - def _define_op(_op_class: Type, decorated=True) -> Type | "Operation": + def _define_op(_op_class: type, decorated=True) -> type | Operation: cls._assert_op_class_ok( f"decorated {cls.value_name()} component", _op_class ) @@ -206,7 +208,7 @@ def _define_op(_op_class: Type, decorated=True) -> Type | "Operation": return _define_op @classmethod - def _assert_op_class_ok(cls, value_name: str, op_class: Type): + def _assert_op_class_ok(cls, value_name: str, op_class: type): if not isclass(op_class): raise TypeError( f"{value_name} must be a class, but got {type(op_class).__name__}" diff --git a/xrlint/plugin.py b/xrlint/plugin.py index d5b6d11..915d7ec 100644 --- a/xrlint/plugin.py +++ b/xrlint/plugin.py @@ -2,8 +2,9 @@ # This software is distributed under the terms and conditions of the # MIT license (https://mit-license.org/). +from collections.abc import Callable from dataclasses import dataclass, field -from typing import Any, Callable, Literal, Type +from typing import Any, Literal from xrlint.config import Config, ConfigLike, ConfigObject from xrlint.processor import Processor, ProcessorOp, define_processor @@ -72,8 +73,8 @@ def define_rule( type: Literal["problem", "suggestion", "layout"] = "problem", description: str | None = None, docs_url: str | None = None, - op_class: Type[RuleOp] | None = None, - ) -> Callable[[Any], Type[RuleOp]] | None: + op_class: type[RuleOp] | None = None, + ) -> Callable[[Any], type[RuleOp]] | None: """Decorator to define a plugin rule. The method registers a new rule with the plugin. @@ -95,7 +96,7 @@ def define_processor( self, name: str | None = None, version: str = "0.0.0", - op_class: Type[ProcessorOp] | None = None, + op_class: type[ProcessorOp] | None = None, ): """Decorator to define a plugin processor. The method registers a new processor with the plugin. diff --git a/xrlint/plugins/acdd/rules/attributes.py b/xrlint/plugins/acdd/rules/attributes.py index be265fb..cf3aa3e 100644 --- a/xrlint/plugins/acdd/rules/attributes.py +++ b/xrlint/plugins/acdd/rules/attributes.py @@ -1,8 +1,8 @@ from typing import Literal from xrlint.node import DatasetNode -from xrlint.rule import RuleContext, RuleOp from xrlint.plugins.acdd.plugin import plugin +from xrlint.rule import RuleContext, RuleOp attrs_1_0_high_rec = { "title": "A short phrase or sentence describing the dataset. In many discovery systems, the title will be displayed in the results list from a search, and therefore should be human readable and reasonable to display in a list of such names. This attribute is also recommended by the NetCDF Users Guide and the CF conventions.", @@ -102,7 +102,7 @@ class AttributesRule(RuleOp): - attrs = {} + attrs = {} # noqa: RUF012 level: Literal["highly recommended", "recommended", "suggested"] = "recommended" def validate_dataset(self, ctx: RuleContext, node: DatasetNode): diff --git a/xrlint/plugins/acdd/rules/conventions.py b/xrlint/plugins/acdd/rules/conventions.py index 662339d..52807a2 100644 --- a/xrlint/plugins/acdd/rules/conventions.py +++ b/xrlint/plugins/acdd/rules/conventions.py @@ -1,6 +1,6 @@ from xrlint.node import DatasetNode -from xrlint.rule import RuleContext, RuleOp from xrlint.plugins.acdd.plugin import plugin +from xrlint.rule import RuleContext, RuleOp @plugin.define_rule( diff --git a/xrlint/plugins/acdd/rules/iso_dates.py b/xrlint/plugins/acdd/rules/iso_dates.py index 50e0913..c4a6be9 100644 --- a/xrlint/plugins/acdd/rules/iso_dates.py +++ b/xrlint/plugins/acdd/rules/iso_dates.py @@ -1,7 +1,8 @@ import isodate + from xrlint.node import DatasetNode -from xrlint.rule import RuleContext, RuleOp from xrlint.plugins.acdd.plugin import plugin +from xrlint.rule import RuleContext, RuleOp def datetime_is_iso(date_str): diff --git a/xrlint/plugins/acdd/rules/metadata_link.py b/xrlint/plugins/acdd/rules/metadata_link.py index 299bca0..042eba1 100644 --- a/xrlint/plugins/acdd/rules/metadata_link.py +++ b/xrlint/plugins/acdd/rules/metadata_link.py @@ -1,6 +1,6 @@ from xrlint.node import DatasetNode -from xrlint.rule import RuleContext, RuleOp from xrlint.plugins.acdd.plugin import plugin +from xrlint.rule import RuleContext, RuleOp @plugin.define_rule( diff --git a/xrlint/plugins/acdd/rules/no_id_blanks.py b/xrlint/plugins/acdd/rules/no_id_blanks.py index e0141b7..af9ac9f 100644 --- a/xrlint/plugins/acdd/rules/no_id_blanks.py +++ b/xrlint/plugins/acdd/rules/no_id_blanks.py @@ -1,6 +1,6 @@ from xrlint.node import DatasetNode -from xrlint.rule import RuleContext, RuleOp from xrlint.plugins.acdd.plugin import plugin +from xrlint.rule import RuleContext, RuleOp @plugin.define_rule( diff --git a/xrlint/plugins/core/__init__.py b/xrlint/plugins/core/__init__.py index f48004b..320cf5b 100644 --- a/xrlint/plugins/core/__init__.py +++ b/xrlint/plugins/core/__init__.py @@ -38,7 +38,7 @@ def export_plugin() -> Plugin: "all", { "name": "all", - "rules": {rule_id: "error" for rule_id in plugin.rules.keys()}, + "rules": {rule_id: "error" for rule_id in plugin.rules}, }, ) diff --git a/xrlint/plugins/core/rules/content_desc.py b/xrlint/plugins/core/rules/content_desc.py index 513b218..60310e5 100644 --- a/xrlint/plugins/core/rules/content_desc.py +++ b/xrlint/plugins/core/rules/content_desc.py @@ -6,8 +6,8 @@ from xrlint.node import DatasetNode, VariableNode from xrlint.plugins.core.plugin import plugin -from xrlint.util.attrs import hierarchical_attrs from xrlint.rule import RuleContext, RuleExit, RuleOp +from xrlint.util.attrs import hierarchical_attrs from xrlint.util.schema import schema DEFAULT_GLOBAL_ATTRS = ["title", "history"] diff --git a/xrlint/plugins/core/rules/conventions.py b/xrlint/plugins/core/rules/conventions.py index 0effdef..17d8879 100644 --- a/xrlint/plugins/core/rules/conventions.py +++ b/xrlint/plugins/core/rules/conventions.py @@ -6,8 +6,8 @@ from xrlint.node import DatasetNode from xrlint.plugins.core.plugin import plugin -from xrlint.util.attrs import hierarchical_attrs from xrlint.rule import RuleContext, RuleExit, RuleOp +from xrlint.util.attrs import hierarchical_attrs from xrlint.util.schema import schema diff --git a/xrlint/plugins/core/rules/coords_for_dims.py b/xrlint/plugins/core/rules/coords_for_dims.py index 30d0e23..c562a34 100644 --- a/xrlint/plugins/core/rules/coords_for_dims.py +++ b/xrlint/plugins/core/rules/coords_for_dims.py @@ -37,8 +37,10 @@ def validate_dataset(self, ctx: RuleContext, node: DatasetNode): f"{format_item(n, 'Data variable dimension')} without" f" coordinates: {', '.join(no_coord_dims)}.", suggestions=[ - f"Add corresponding {format_item(n, 'coordinate variable')}" - f" to dataset:" - f" {', '.join(f'{d}[{dataset.sizes[d]}]' for d in no_coord_dims)}." + ( + f"Add corresponding {format_item(n, 'coordinate variable')}" + f" to dataset:" + f" {', '.join(f'{d}[{dataset.sizes[d]}]' for d in no_coord_dims)}." + ) ], ) diff --git a/xrlint/plugins/core/rules/time_coordinate.py b/xrlint/plugins/core/rules/time_coordinate.py index daeb784..797ca64 100644 --- a/xrlint/plugins/core/rules/time_coordinate.py +++ b/xrlint/plugins/core/rules/time_coordinate.py @@ -166,8 +166,10 @@ def validate_variable(self, ctx: RuleContext, node: VariableNode): f"Missing timezone in {source} 'units': {units!r}.", suggestions=[ _units_format_suggestion(), - f"Append timezone specification, e.g., use" - f" {' '.join(units_parts[:-1] + ['+0:00'])!r}.", + ( + f"Append timezone specification, e.g., use" + f" {' '.join(units_parts[:-1] + ['+0:00'])!r}." + ), ], ) diff --git a/xrlint/plugins/xcube/__init__.py b/xrlint/plugins/xcube/__init__.py index 31ed245..bce9aee 100644 --- a/xrlint/plugins/xcube/__init__.py +++ b/xrlint/plugins/xcube/__init__.py @@ -59,9 +59,7 @@ def export_plugin() -> Plugin: [ *common_configs, { - "rules": { - f"xcube/{rule_id}": "error" for rule_id in plugin.rules.keys() - }, + "rules": {f"xcube/{rule_id}": "error" for rule_id in plugin.rules}, }, ], ) diff --git a/xrlint/plugins/xcube/processors/mldataset.py b/xrlint/plugins/xcube/processors/mldataset.py index f63c020..92847c6 100644 --- a/xrlint/plugins/xcube/processors/mldataset.py +++ b/xrlint/plugins/xcube/processors/mldataset.py @@ -26,7 +26,7 @@ @plugin.define_processor("multi-level-dataset") class MultiLevelDatasetProcessor(ProcessorOp): - f"""This processor should be used with `files: [{ML_FILE_PATTERN}"]`.""" + f"""This processor should be used with `files: ["{ML_FILE_PATTERN}"]`.""" # noqa: B021 def preprocess( self, file_path: str, opener_options: dict[str, Any] @@ -50,12 +50,12 @@ def preprocess( # with fs.open(f"{fs_path}/.zgroup") as stream: # group_props = json.load(stream) - level_paths, num_levels = parse_levels(fs, file_path, file_names) + level_paths, _num_levels = parse_levels(fs, file_path, file_names) engine = opener_options.pop("engine", "zarr") level_datasets: list[xr.Dataset | None] = [] - for level, level_path in level_paths.items(): + for level_path in level_paths.values(): level_dataset = xr.open_dataset(level_path, engine=engine, **opener_options) level_datasets.append((level_dataset, level_path)) diff --git a/xrlint/plugins/xcube/rules/increasing_time.py b/xrlint/plugins/xcube/rules/increasing_time.py index 8a3a239..dd4bd6a 100644 --- a/xrlint/plugins/xcube/rules/increasing_time.py +++ b/xrlint/plugins/xcube/rules/increasing_time.py @@ -24,7 +24,7 @@ def validate_variable(self, ctx: RuleContext, node: VariableNode): array = node.array if node.in_coords() and node.name == "time" and array.dims == ("time",): diff_array: np.ndarray = array.diff("time").values - if not np.count_nonzero(diff_array > 0) == diff_array.size: + if np.count_nonzero(diff_array > 0) != diff_array.size: check_indexes(ctx, diff_array == 0, "Duplicate") check_indexes(ctx, diff_array < 0, "Backsliding") raise RuleExit # No need to apply rule any further diff --git a/xrlint/plugins/xcube/rules/ml_dataset_meta.py b/xrlint/plugins/xcube/rules/ml_dataset_meta.py index 3e51b0f..acf9a69 100644 --- a/xrlint/plugins/xcube/rules/ml_dataset_meta.py +++ b/xrlint/plugins/xcube/rules/ml_dataset_meta.py @@ -43,10 +43,12 @@ def validate_dataset(self, ctx: RuleContext, node: DatasetNode): ctx.report( f"Missing {ML_META_FILENAME!r} meta-info file.", suggestions=[ - f"Add {ML_META_FILENAME!r} meta-info file." - f" Without the meta-info the dataset cannot be reliably extended" - f" as the aggregation method used for each variable must be" - f" specified." + ( + f"Add {ML_META_FILENAME!r} meta-info file." + f" Without the meta-info the dataset cannot be reliably extended" + f" as the aggregation method used for each variable must be" + f" specified." + ) ], ) return @@ -82,7 +84,7 @@ def validate_dataset(self, ctx: RuleContext, node: DatasetNode): f"Missing value for variable {var_name!r}" f" in 'agg_methods' of {ML_META_FILENAME!r} meta-info." ) - for var_name in meta.agg_methods.keys(): + for var_name in meta.agg_methods: if var_name not in node.dataset: ctx.report( f"Variable {var_name!r} not found in dataset, but specified" diff --git a/xrlint/plugins/xcube/rules/no_chunked_coords.py b/xrlint/plugins/xcube/rules/no_chunked_coords.py index 8b68135..aea4db2 100644 --- a/xrlint/plugins/xcube/rules/no_chunked_coords.py +++ b/xrlint/plugins/xcube/rules/no_chunked_coords.py @@ -26,14 +26,14 @@ ), schema=schema( "object", - properties=dict( - limit=schema( + properties={ + "limit": schema( "integer", minimum=0, default=DEFAULT_LIMIT, title="Acceptable number of chunks", ) - ), + }, ), ) class NoChunkedCoords(RuleOp): diff --git a/xrlint/plugins/xcube/rules/time_naming.py b/xrlint/plugins/xcube/rules/time_naming.py index e9f6a4b..935c67a 100644 --- a/xrlint/plugins/xcube/rules/time_naming.py +++ b/xrlint/plugins/xcube/rules/time_naming.py @@ -27,7 +27,7 @@ def validate_dataset(self, ctx: RuleContext, node: DatasetNode): for var_name, var in node.dataset.coords.items() if var_name != TIME_NAME and _is_time_coord(var_name, var) } - for var_name, var in time_vars.items(): + for var_name in time_vars: ctx.report( f"The coordinate {var_name!r} should be named {TIME_NAME!r}.", suggestions=[f"Rename {var_name!r} to {TIME_NAME!r}."], diff --git a/xrlint/plugins/xcube/util.py b/xrlint/plugins/xcube/util.py index b574af6..ca04514 100644 --- a/xrlint/plugins/xcube/util.py +++ b/xrlint/plugins/xcube/util.py @@ -85,7 +85,7 @@ def get_spatial_size( dataset: xr.Dataset, ) -> tuple[tuple[Hashable, int], tuple[Hashable, int]] | None: """Return (x_size, y_size) for given dataset.""" - for k, v in dataset.data_vars.items(): + for v in dataset.data_vars.values(): if is_spatial_var(v): y_name, x_name = v.dims[-2:] x_size = dataset.sizes[x_name] @@ -110,11 +110,8 @@ def resolve_path(path: str, root_path: str | None = None) -> str: def is_absolute_path(path: str) -> bool: return ( # Unix abs path - path.startswith("/") - # URL + path.startswith(("/", "\\\\")) or "://" in path - # Windows abs paths - or path.startswith("\\\\") or path.find(":\\", 1) == 1 or path.find(":/", 1) == 1 ) diff --git a/xrlint/processor.py b/xrlint/processor.py index 52759aa..d0263cf 100644 --- a/xrlint/processor.py +++ b/xrlint/processor.py @@ -3,8 +3,9 @@ # MIT license (https://mit-license.org/). from abc import ABC, abstractmethod +from collections.abc import Callable from dataclasses import dataclass -from typing import Any, Callable, Type +from typing import Any import xarray as xr @@ -87,7 +88,7 @@ class Processor(Operation): meta: ProcessorMeta """Information about the processor.""" - op_class: Type[ProcessorOp] + op_class: type[ProcessorOp] """A class that implements the processor operations.""" # Not yet: @@ -95,11 +96,11 @@ class Processor(Operation): # """`True` if this processor supports auto-fixing of datasets.""" @classmethod - def meta_class(cls) -> Type: + def meta_class(cls) -> type: return ProcessorMeta @classmethod - def op_base_class(cls) -> Type: + def op_base_class(cls) -> type: return ProcessorOp @classmethod @@ -111,8 +112,8 @@ def define_processor( name: str | None = None, version: str = "0.0.0", registry: dict[str, Processor] | None = None, - op_class: Type[ProcessorOp] | None = None, -) -> Callable[[Any], Type[ProcessorOp]] | Processor: + op_class: type[ProcessorOp] | None = None, +) -> Callable[[Any], type[ProcessorOp]] | Processor: """Define a processor. This function can be used to decorate your processor operation class @@ -141,5 +142,5 @@ def define_processor( a class derived from [ProcessorOp][xrlint.processor.ProcessorOp]. """ return Processor.define_operation( - op_class, registry=registry, meta_kwargs=dict(name=name, version=version) + op_class, registry=registry, meta_kwargs={"name": name, "version": version} ) diff --git a/xrlint/rule.py b/xrlint/rule.py index 6b53cc3..ff48cc9 100644 --- a/xrlint/rule.py +++ b/xrlint/rule.py @@ -3,9 +3,9 @@ # MIT license (https://mit-license.org/). from abc import ABC, abstractmethod -from collections.abc import MutableMapping, Sequence +from collections.abc import Callable, MutableMapping, Sequence from dataclasses import dataclass, field -from typing import Any, Callable, Literal, Type +from typing import Any, Literal import xarray as xr @@ -223,17 +223,17 @@ class that implements the rule's logic. meta: RuleMeta """Rule metadata of type `RuleMeta`.""" - op_class: Type[RuleOp] + op_class: type[RuleOp] """The class the implements the rule's validation operation. The class must implement the `RuleOp` interface. """ @classmethod - def meta_class(cls) -> Type: + def meta_class(cls) -> type: return RuleMeta @classmethod - def op_base_class(cls) -> Type: + def op_base_class(cls) -> type: return RuleOp @classmethod @@ -337,8 +337,8 @@ def define_rule( docs_url: str | None = None, schema: dict[str, Any] | list[dict[str, Any]] | bool | None = None, registry: MutableMapping[str, Rule] | None = None, - op_class: Type[RuleOp] | None = None, -) -> Callable[[Any], Type[RuleOp]] | Rule: + op_class: type[RuleOp] | None = None, +) -> Callable[[Any], type[RuleOp]] | Rule: """Define a rule. This function can be used to decorate your rule operation class @@ -373,12 +373,12 @@ def define_rule( return Rule.define_operation( op_class, registry=registry, - meta_kwargs=dict( - name=name, - version=version, - description=description, - docs_url=docs_url, - type=type if type else "problem", - schema=schema, - ), + meta_kwargs={ + "name": name, + "version": version, + "description": description, + "docs_url": docs_url, + "type": type if type else "problem", + "schema": schema, + }, ) diff --git a/xrlint/testing.py b/xrlint/testing.py index 96dd1e0..bad653c 100644 --- a/xrlint/testing.py +++ b/xrlint/testing.py @@ -3,8 +3,9 @@ # MIT license (https://mit-license.org/). import unittest +from collections.abc import Callable from dataclasses import dataclass -from typing import Any, Callable, Final, Literal, Type +from typing import Any, Final, Literal import xarray as xr @@ -64,7 +65,7 @@ def __init__(self, *, config: ConfigLike = None, **config_props: Any): def run( self, rule_name: str, - rule_op_class: Type[RuleOp], + rule_op_class: type[RuleOp], *, valid: list[RuleTest] | None = None, invalid: list[RuleTest] | None = None, @@ -93,13 +94,13 @@ def run( def define_test( cls, rule_name: str, - rule_op_class: Type[RuleOp], + rule_op_class: type[RuleOp], *, valid: list[RuleTest] | None = None, invalid: list[RuleTest] | None = None, config: ConfigLike = None, **config_props: Any, - ) -> Type[unittest.TestCase]: + ) -> type[unittest.TestCase]: """Create a `unittest.TestCase` class for the given rule and tests. The returned class is derived from `unittest.TestCase` @@ -131,7 +132,7 @@ def define_test( def _create_tests( self, rule_name: str, - rule_op_class: Type[RuleOp], + rule_op_class: type[RuleOp], valid: list[RuleTest] | None, invalid: list[RuleTest] | None, ) -> dict[str, Callable[[unittest.TestCase | None], None]]: @@ -153,12 +154,12 @@ def make_args(checks: list[RuleTest] | None, mode: Literal["valid", "invalid"]): def _create_name_test( self, rule_name: str, - rule_op_class: Type[RuleOp], + rule_op_class: type[RuleOp], ) -> tuple[str, Callable]: test_id = "test_rule_meta" def test_fn(_self: unittest.TestCase): - rule_meta: RuleMeta = getattr(rule_op_class, "meta") + rule_meta: RuleMeta = rule_op_class.meta assert rule_meta.name == rule_name, ( f"rule name expected to be {rule_name!r}, but was {rule_meta.name!r}" ) @@ -169,7 +170,7 @@ def test_fn(_self: unittest.TestCase): def _create_test( self, rule_name: str, - rule_op_class: Type[RuleOp], + rule_op_class: type[RuleOp], test: RuleTest, test_index: int, test_mode: Literal["valid", "invalid"], @@ -189,7 +190,7 @@ def test_fn(_self: unittest.TestCase): def _test_rule( self, rule_name: str, - rule_op_class: Type[RuleOp], + rule_op_class: type[RuleOp], test: RuleTest, test_id: str, test_mode: Literal["valid", "invalid"], diff --git a/xrlint/util/attrs.py b/xrlint/util/attrs.py index 642d0de..cd29d4b 100644 --- a/xrlint/util/attrs.py +++ b/xrlint/util/attrs.py @@ -5,7 +5,7 @@ from collections.abc import Iterator, Mapping from typing import Any -from xrlint.node import DataTreeNode, DatasetNode +from xrlint.node import DatasetNode, DataTreeNode class HierarchicalAttrs(Mapping[str, Any]): diff --git a/xrlint/util/constructible.py b/xrlint/util/constructible.py index e0a7f62..60d5ab5 100644 --- a/xrlint/util/constructible.py +++ b/xrlint/util/constructible.py @@ -11,8 +11,6 @@ Any, Generic, Literal, - Optional, - Type, TypeVar, Union, get_args, @@ -124,7 +122,7 @@ def _from_str(cls, value: str, value_name: str) -> T: raise TypeError(cls._format_type_error(value, value_name)) @classmethod - def _from_class(cls, value: Type, value_name: str) -> T: + def _from_class(cls, value: type, value_name: str) -> T: """Create an instance of this class from a type value. The default implementation raises a `TypeError`. Override to implement a different behaviour. @@ -269,7 +267,7 @@ def class_parameters(cls) -> Mapping[str, Parameter]: return get_class_parameters(cls, forward_refs=cls.forward_refs()) @classmethod - def forward_refs(cls) -> Optional[Mapping[str, type]]: + def forward_refs(cls) -> Mapping[str, type] | None: """Get an extra namespace to be used for resolving parameter type hints. Called from [ValueConstructible._get_class_parameters][]. @@ -331,7 +329,7 @@ def _format_type_error(cls, value: Any, value_name: str) -> str: return format_message_type_of(value_name, value, cls.value_type_name()) -class MappingConstructible(Generic[T], ValueConstructible[T]): +class MappingConstructible(ValueConstructible[T], Generic[T]): """A mixin that makes your classes constructible from mappings, such as a `dict`. diff --git a/xrlint/util/filepattern.py b/xrlint/util/filepattern.py index ebdd39d..e7085ec 100644 --- a/xrlint/util/filepattern.py +++ b/xrlint/util/filepattern.py @@ -40,7 +40,7 @@ def __init__(self, pattern: str, flip_negate: bool = False): self._empty = False self._comment = False self._negate = False - self._dir: Literal[True, None] = None # we cannot know + self._dir: Literal[True] | None = None # we cannot know if not pattern: self._empty = True @@ -80,7 +80,7 @@ def negate(self) -> bool: return self._negate @property - def dir(self) -> Literal[True, None]: + def dir(self) -> Literal[True] | None: """`True` if this matcher's pattern denotes a directory.""" return self._dir diff --git a/xrlint/util/formatting.py b/xrlint/util/formatting.py index 87853aa..8a09bc9 100644 --- a/xrlint/util/formatting.py +++ b/xrlint/util/formatting.py @@ -25,7 +25,7 @@ def format_problems(error_count: int, warning_count: int) -> str: def format_count( - count: int | float, + count: float, singular: str, plural: str | None = None, upper: bool | None = None, @@ -41,7 +41,7 @@ def format_count( def format_item( - count: int | float, + count: float, singular: str, plural: str | None = None, upper: bool | None = None, diff --git a/xrlint/util/importutil.py b/xrlint/util/importutil.py index 6506091..b2d4139 100644 --- a/xrlint/util/importutil.py +++ b/xrlint/util/importutil.py @@ -4,7 +4,8 @@ import importlib import pathlib -from typing import Any, Callable, Type, TypeVar +from collections.abc import Callable +from typing import Any, TypeVar from xrlint.util.formatting import format_message_type_of @@ -48,7 +49,7 @@ def import_value( *, constant: bool = False, factory: Callable[[Any], T] | None = None, - expected_type: Type[T] | None = None, + expected_type: type[T] | None = None, ) -> tuple[T, str]: """Import an exported value from given module reference. diff --git a/xrlint/util/merge.py b/xrlint/util/merge.py index 403e59f..7d0cb08 100644 --- a/xrlint/util/merge.py +++ b/xrlint/util/merge.py @@ -2,7 +2,8 @@ # This software is distributed under the terms and conditions of the # MIT license (https://mit-license.org/). -from typing import Any, Callable +from collections.abc import Callable +from typing import Any def merge_values( diff --git a/xrlint/util/schema.py b/xrlint/util/schema.py index bf8c5c7..bf37edd 100644 --- a/xrlint/util/schema.py +++ b/xrlint/util/schema.py @@ -40,10 +40,10 @@ def schema( title: str | None = None, description: str | None = None, # "integer", "number" - minimum: int | float | None = None, - maximum: int | float | None = None, - exclusiveMinimum: int | float | None = None, - exclusiveMaximum: int | float | None = None, + minimum: float | None = None, + maximum: float | None = None, + exclusiveMinimum: float | None = None, + exclusiveMaximum: float | None = None, # "array" items: list[JsonSchema] | JsonSchema | None = None, # "object" @@ -54,22 +54,22 @@ def schema( """Helper function so you have keyword-arguments for creating schemas.""" return { k: v - for k, v in dict( - type=_parse_type(type), - default=default, - const=const, - enum=enum, - minimum=minimum, - maximum=maximum, - exclusiveMinimum=exclusiveMinimum, - exclusiveMaximum=exclusiveMaximum, - items=items, - properties=properties, - additionalProperties=False if additionalProperties is False else None, - required=required, - title=title, - description=description, - ).items() + for k, v in { + "type": _parse_type(type), + "default": default, + "const": const, + "enum": enum, + "minimum": minimum, + "maximum": maximum, + "exclusiveMinimum": exclusiveMinimum, + "exclusiveMaximum": exclusiveMaximum, + "items": items, + "properties": properties, + "additionalProperties": False if additionalProperties is False else None, + "required": required, + "title": title, + "description": description, + }.items() if v is not None } diff --git a/xrlint/util/serializable.py b/xrlint/util/serializable.py index 4f9a4b9..f6f172c 100644 --- a/xrlint/util/serializable.py +++ b/xrlint/util/serializable.py @@ -2,8 +2,9 @@ # This software is distributed under the terms and conditions of the # MIT license (https://mit-license.org/). +from collections.abc import Mapping, Sequence from dataclasses import fields, is_dataclass -from typing import Any, Final, Mapping, Sequence, TypeAlias +from typing import Any, Final, TypeAlias from xrlint.util.formatting import format_message_type_of @@ -98,5 +99,5 @@ def _is_non_protected_property_name(cls, key: Any) -> bool: isinstance(key, str) and key.isidentifier() and not key[0].isupper() - and not key[0] == "_" + and key[0] != "_" )