From 1f4004c6576b74ce02e4837d1458e45f64e78a49 Mon Sep 17 00:00:00 2001 From: Dimitri Papadopoulos <3234522+DimitriPapadopoulos@users.noreply.github.com> Date: Mon, 11 Mar 2024 21:47:04 +0100 Subject: [PATCH 1/7] Enable ruff/bugbear rules (B) As suggested by Repo-Review. --- pyproject.toml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/pyproject.toml b/pyproject.toml index 904c974424..ea57fa0478 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -103,6 +103,11 @@ exclude = [ "docs" ] +[tool.ruff.lint] +extend-select = [ + "B" +] + [tool.black] line-length = 100 exclude = ''' From 4abb3cd12d3b396aebdeedfba0c6264a9579dfd6 Mon Sep 17 00:00:00 2001 From: Dimitri Papadopoulos <3234522+DimitriPapadopoulos@users.noreply.github.com> Date: Mon, 11 Mar 2024 21:34:13 +0100 Subject: [PATCH 2/7] Fix ruff/bugbear issue (B007) B007 Loop control variable `key` not used within loop body https://docs.astral.sh/ruff/rules/unused-loop-control-variable/ --- zarr/tests/test_meta.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/zarr/tests/test_meta.py b/zarr/tests/test_meta.py index c85d3f923f..a91e07b531 100644 --- a/zarr/tests/test_meta.py +++ b/zarr/tests/test_meta.py @@ -601,7 +601,7 @@ def test_metadata3_exceptions(): required = ["zarr_format", "metadata_encoding", "metadata_key_suffix", "extensions"] for key in required: meta = copy.copy(_default_entry_point_metadata_v3) - meta.pop("zarr_format") + meta.pop(key) with pytest.raises(ValueError): # cannot encode metadata that is missing a required key Metadata3.encode_hierarchy_metadata(meta) From 381b7b00431062b24ee1b5466ede22a22c4cbd04 Mon Sep 17 00:00:00 2001 From: Dimitri Papadopoulos <3234522+DimitriPapadopoulos@users.noreply.github.com> Date: Mon, 11 Mar 2024 21:36:04 +0100 Subject: [PATCH 3/7] Fix ruff/bugbear issue (B015) B015 Pointless comparison. Did you mean to assign a value? Otherwise, prepend `assert` or remove it. https://docs.astral.sh/ruff/rules/useless-comparison/ --- zarr/tests/test_storage_v3.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/zarr/tests/test_storage_v3.py b/zarr/tests/test_storage_v3.py index c096f9cb02..e8675786e0 100644 --- a/zarr/tests/test_storage_v3.py +++ b/zarr/tests/test_storage_v3.py @@ -286,7 +286,7 @@ def test_rename_nonexisting(self): def test_get_partial_values(self): store = self.create_store() - store.supports_efficient_get_partial_values in [True, False] + assert store.supports_efficient_get_partial_values in [True, False] store[data_root + "foo"] = b"abcdefg" store[data_root + "baz"] = b"z" assert [b"a"] == store.get_partial_values([(data_root + "foo", (0, 1))]) From 80399234fd8d0bb137725b9b7e416a1ccdbe841a Mon Sep 17 00:00:00 2001 From: Dimitri Papadopoulos <3234522+DimitriPapadopoulos@users.noreply.github.com> Date: Mon, 11 Mar 2024 21:53:22 +0100 Subject: [PATCH 4/7] Fix ruff/bugbear issues (B028) B028 No explicit `stacklevel` keyword argument found https://docs.astral.sh/ruff/rules/no-explicit-stacklevel/ --- zarr/creation.py | 2 +- zarr/n5.py | 18 +++++++++++++++--- zarr/storage.py | 6 +++++- 3 files changed, 21 insertions(+), 5 deletions(-) diff --git a/zarr/creation.py b/zarr/creation.py index c541531d54..9b2b1d6d4c 100644 --- a/zarr/creation.py +++ b/zarr/creation.py @@ -297,7 +297,7 @@ def _kwargs_compat(compressor, fill_value, kwargs): # ignore other keyword arguments for k in kwargs: - warn(f"ignoring keyword argument {k!r}") + warn(f"ignoring keyword argument {k!r}", stacklevel=2) return compressor, fill_value diff --git a/zarr/n5.py b/zarr/n5.py index fdd3d5babf..bb80dba3cc 100644 --- a/zarr/n5.py +++ b/zarr/n5.py @@ -125,7 +125,11 @@ def __setitem__(self, key: str, value: Any): for k in n5_keywords: if k in zarr_attrs: - warnings.warn(f"Attribute {k} is a reserved N5 keyword", UserWarning) + warnings.warn( + f"Attribute {k} is a reserved N5 keyword", + UserWarning, + stacklevel=2, + ) # remove previous user attributes for k in list(n5_attrs.keys()): @@ -327,7 +331,10 @@ class N5FSStore(FSStore): def __init__(self, *args, **kwargs): if "dimension_separator" in kwargs: kwargs.pop("dimension_separator") - warnings.warn("Keyword argument `dimension_separator` will be ignored") + warnings.warn( + "Keyword argument `dimension_separator` will be ignored", + stacklevel=2, + ) dimension_separator = "." super().__init__(*args, dimension_separator=dimension_separator, **kwargs) @@ -411,7 +418,11 @@ def __setitem__(self, key: str, value: Any): for k in n5_keywords: if k in zarr_attrs.keys(): - warnings.warn(f"Attribute {k} is a reserved N5 keyword", UserWarning) + warnings.warn( + f"Attribute {k} is a reserved N5 keyword", + UserWarning, + stacklevel=2, + ) # replace previous user attributes for k in list(n5_attrs.keys()): @@ -711,6 +722,7 @@ def compressor_config_to_n5(compressor_config: Optional[Dict[str, Any]]) -> Dict "Not all N5 implementations support lzma compression (yet). You " "might not be able to open the dataset with another N5 library.", RuntimeWarning, + stacklevel=2, ) n5_config["format"] = _compressor_config["format"] n5_config["check"] = _compressor_config["check"] diff --git a/zarr/storage.py b/zarr/storage.py index 10f55f0ba3..a3ee8d87d9 100644 --- a/zarr/storage.py +++ b/zarr/storage.py @@ -588,11 +588,15 @@ def _init_array_metadata( "missing object_codec for object array; this will raise a " "ValueError in version 3.0", FutureWarning, + stacklevel=2, ) else: filters_config.insert(0, object_codec.get_config()) elif object_codec is not None: - warnings.warn("an object_codec is only needed for object arrays") + warnings.warn( + "an object_codec is only needed for object arrays", + stacklevel=2, + ) # use null to indicate no filters if not filters_config: From e3ceb5c010097ef22103d97d40b1a2b2666468ce Mon Sep 17 00:00:00 2001 From: Dimitri Papadopoulos <3234522+DimitriPapadopoulos@users.noreply.github.com> Date: Mon, 11 Mar 2024 21:43:45 +0100 Subject: [PATCH 5/7] Fix ruff/bugbear issues (B904) B904 Within an `except` clause, raise exceptions with `raise ... from err` or `raise ... from None` to distinguish them from errors in exception handling https://docs.astral.sh/ruff/rules/raise-without-from-inside-except/ --- zarr/_storage/absstore.py | 8 ++++---- zarr/_storage/v3_storage_transformers.py | 8 ++++---- zarr/core.py | 4 ++-- zarr/hierarchy.py | 10 +++++----- zarr/indexing.py | 2 +- zarr/meta.py | 6 +++--- zarr/meta_v1.py | 2 +- zarr/n5.py | 4 ++-- zarr/storage.py | 10 +++++----- zarr/util.py | 12 ++++++------ 10 files changed, 33 insertions(+), 33 deletions(-) diff --git a/zarr/_storage/absstore.py b/zarr/_storage/absstore.py index 217b2a29e0..ce3d32cac2 100644 --- a/zarr/_storage/absstore.py +++ b/zarr/_storage/absstore.py @@ -143,8 +143,8 @@ def __getitem__(self, key): blob_name = self._append_path_to_prefix(key) try: return self.client.download_blob(blob_name).readall() - except ResourceNotFoundError: - raise KeyError(f"Blob {blob_name} not found") + except ResourceNotFoundError as e: + raise KeyError(f"Blob {blob_name} not found") from e def __setitem__(self, key, value): value = ensure_bytes(value) @@ -156,8 +156,8 @@ def __delitem__(self, key): try: self.client.delete_blob(self._append_path_to_prefix(key)) - except ResourceNotFoundError: - raise KeyError(f"Blob {key} not found") + except ResourceNotFoundError as e: + raise KeyError(f"Blob {key} not found") from e def __eq__(self, other): return ( diff --git a/zarr/_storage/v3_storage_transformers.py b/zarr/_storage/v3_storage_transformers.py index 37e56f8ecd..00467d44f9 100644 --- a/zarr/_storage/v3_storage_transformers.py +++ b/zarr/_storage/v3_storage_transformers.py @@ -183,8 +183,8 @@ def __getitem__(self, key): shard_key, chunk_subkey = self._key_to_shard(key) try: full_shard_value = self.inner_store[shard_key] - except KeyError: - raise KeyError(key) + except KeyError as e: + raise KeyError(key) from e index = self._get_index_from_buffer(full_shard_value) chunk_slice = index.get_chunk_slice(chunk_subkey) if chunk_slice is not None: @@ -265,8 +265,8 @@ def __delitem__(self, key): shard_key, chunk_subkey = self._key_to_shard(key) try: index = self._get_index_from_store(shard_key) - except KeyError: - raise KeyError(key) + except KeyError as e: + raise KeyError(key) from e index.set_chunk_slice(chunk_subkey, None) diff --git a/zarr/core.py b/zarr/core.py index 1bd081acee..6aa86b6465 100644 --- a/zarr/core.py +++ b/zarr/core.py @@ -200,8 +200,8 @@ def _load_metadata_nosync(self): try: mkey = _prefix_to_array_key(self._store, self._key_prefix) meta_bytes = self._store[mkey] - except KeyError: - raise ArrayNotFoundError(self._path) + except KeyError as e: + raise ArrayNotFoundError(self._path) from e else: # decode and store metadata as instance members meta = self._store._metadata_class.decode_array_metadata(meta_bytes) diff --git a/zarr/hierarchy.py b/zarr/hierarchy.py index 0fb07dd620..8894a5ed57 100644 --- a/zarr/hierarchy.py +++ b/zarr/hierarchy.py @@ -187,16 +187,16 @@ def __init__( mkey = _prefix_to_group_key(self._store, self._key_prefix) assert not mkey.endswith("root/.group") meta_bytes = store[mkey] - except KeyError: + except KeyError as e: if self._version == 2: - raise GroupNotFoundError(path) + raise GroupNotFoundError(path) from e else: implicit_prefix = meta_root + self._key_prefix if self._store.list_prefix(implicit_prefix): # implicit group does not have any metadata self._meta = None else: - raise GroupNotFoundError(path) + raise GroupNotFoundError(path) from e else: self._meta = self._store._metadata_class.decode_group_metadata(meta_bytes) @@ -536,8 +536,8 @@ def __getattr__(self, item): # allow access to group members via dot notation try: return self.__getitem__(item) - except KeyError: - raise AttributeError + except KeyError as e: + raise AttributeError from e def __dir__(self): # noinspection PyUnresolvedReferences diff --git a/zarr/indexing.py b/zarr/indexing.py index 9889fcadad..2f2402fe27 100644 --- a/zarr/indexing.py +++ b/zarr/indexing.py @@ -932,7 +932,7 @@ def check_fields(fields, dtype): # multiple field selection out_dtype = np.dtype([(f, dtype[f]) for f in fields]) except KeyError as e: - raise IndexError(f"invalid 'fields' argument, field not found: {e!r}") + raise IndexError(f"invalid 'fields' argument, field not found: {e!r}") from e else: return out_dtype else: diff --git a/zarr/meta.py b/zarr/meta.py index 4b360270de..d8aefa3273 100644 --- a/zarr/meta.py +++ b/zarr/meta.py @@ -310,8 +310,8 @@ def decode_dtype(cls, d, validate=True): # extract the type from the extension info try: d = d["type"] - except KeyError: - raise KeyError("Extended dtype info must provide a key named 'type'.") + except KeyError as e: + raise KeyError("Extended dtype info must provide a key named 'type'.") from e d = cls._decode_dtype_descr(d) dtype = np.dtype(d) if validate: @@ -518,7 +518,7 @@ def decode_array_metadata(cls, s: Union[MappingType, bytes, str]) -> MappingType meta["storage_transformers"] = storage_transformers except Exception as e: - raise MetadataError(f"error decoding metadata: {e}") + raise MetadataError(f"error decoding metadata: {e}") from e else: return meta diff --git a/zarr/meta_v1.py b/zarr/meta_v1.py index 65bfd3488e..714f55f477 100644 --- a/zarr/meta_v1.py +++ b/zarr/meta_v1.py @@ -23,7 +23,7 @@ def decode_metadata(b): order=meta["order"], ) except Exception as e: - raise MetadataError(f"error decoding metadata: {e}") + raise MetadataError(f"error decoding metadata: {e}") from e else: return meta diff --git a/zarr/n5.py b/zarr/n5.py index bb80dba3cc..3d3e9afa26 100644 --- a/zarr/n5.py +++ b/zarr/n5.py @@ -608,8 +608,8 @@ def array_metadata_to_n5(array_metadata: Dict[str, Any], top_level=False) -> Dic array_metadata["n5"] = N5_FORMAT try: dtype = np.dtype(array_metadata["dataType"]) - except TypeError: - raise TypeError(f"Data type {array_metadata['dataType']} is not supported by N5") + except TypeError as e: + raise TypeError(f"Data type {array_metadata['dataType']} is not supported by N5") from e array_metadata["dataType"] = dtype.name array_metadata["dimensions"] = array_metadata["dimensions"][::-1] diff --git a/zarr/storage.py b/zarr/storage.py index a3ee8d87d9..73a5f5be71 100644 --- a/zarr/storage.py +++ b/zarr/storage.py @@ -872,8 +872,8 @@ def __getitem__(self, item: str): parent, key = self._get_parent(item) try: value = parent[key] - except KeyError: - raise KeyError(item) + except KeyError as e: + raise KeyError(item) from e else: if isinstance(value, self.cls): raise KeyError(item) @@ -891,8 +891,8 @@ def __delitem__(self, item: str): parent, key = self._get_parent(item) try: del parent[key] - except KeyError: - raise KeyError(item) + except KeyError as e: + raise KeyError(item) from e def __contains__(self, item: str): # type: ignore[override] try: @@ -1140,7 +1140,7 @@ def __setitem__(self, key, value): os.makedirs(dir_path) except OSError as e: if e.errno != errno.EEXIST: - raise KeyError(key) + raise KeyError(key) from e # write to temporary file # note we're not using tempfile.NamedTemporaryFile to avoid restrictive file permissions diff --git a/zarr/util.py b/zarr/util.py index e58aed80ab..8a96f92c24 100644 --- a/zarr/util.py +++ b/zarr/util.py @@ -198,11 +198,11 @@ def normalize_dtype(dtype: Union[str, np.dtype], object_codec) -> Tuple[np.dtype args = [] try: object_codec = codec_registry[codec_id](*args) - except KeyError: # pragma: no cover + except KeyError as e: # pragma: no cover raise ValueError( f"codec {codec_id!r} for object type {key!r} is not " f"available; please provide an object_codec manually" - ) + ) from e return dtype, object_codec dtype = np.dtype(dtype) @@ -332,7 +332,7 @@ def normalize_fill_value(fill_value, dtype: np.dtype): raise ValueError( f"fill_value {fill_value!r} is not valid for dtype {dtype}; " f"nested exception: {e}" - ) + ) from e return fill_value @@ -492,13 +492,13 @@ def tree_widget_sublist(node, root=False, expand=False): def tree_widget(group, expand, level): try: import ipytree - except ImportError as error: + except ImportError as e: raise ImportError( - f"{error}: Run `pip install zarr[jupyter]` or `conda install ipytree`" + f"{e}: Run `pip install zarr[jupyter]` or `conda install ipytree`" f"to get the required ipytree dependency for displaying the tree " f"widget. If using jupyterlab<3, you also need to run " f"`jupyter labextension install ipytree`" - ) + ) from e result = ipytree.Tree() root = TreeNode(group, level=level) From 0e84d1f107b95ad6466e8e5d13ecbb1c1be81a9d Mon Sep 17 00:00:00 2001 From: Dimitri Papadopoulos <3234522+DimitriPapadopoulos@users.noreply.github.com> Date: Mon, 11 Mar 2024 22:04:49 +0100 Subject: [PATCH 6/7] Document changes in docs/release.rst --- docs/release.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/release.rst b/docs/release.rst index 811ede3d58..f5f6b25adc 100644 --- a/docs/release.rst +++ b/docs/release.rst @@ -35,6 +35,9 @@ Docs Maintenance ~~~~~~~~~~~ +* Enable ruff/bugbear rules (B) and fix issues. + By :user:`Dimitri Papadopoulos Orfanos ` :issue:`1702`. + Deprecations ~~~~~~~~~~~~ From 2c392172dbe1d07e065ee7873175e67a6723d298 Mon Sep 17 00:00:00 2001 From: Dimitri Papadopoulos <3234522+DimitriPapadopoulos@users.noreply.github.com> Date: Sun, 5 May 2024 18:36:09 +0200 Subject: [PATCH 7/7] Disable ruff/bugbear rule (B017) B017 `pytest.raises(Exception)` should be considered evil https://docs.astral.sh/ruff/rules/assert-raises-exception/ --- zarr/tests/test_storage.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/zarr/tests/test_storage.py b/zarr/tests/test_storage.py index ae8a56fa61..da690f5959 100644 --- a/zarr/tests/test_storage.py +++ b/zarr/tests/test_storage.py @@ -1353,13 +1353,13 @@ def test_exceptions(self, memory_store): # no exception from FSStore.getitems getting KeyError assert group.store.getitems(["foo"], contexts={}) == {} # exception from FSStore.getitems getting AttributeError - with pytest.raises(Exception): + with pytest.raises(Exception): # noqa: B017 group.store.getitems(["x/0"], contexts={}) # exception from FSStore.getitems getting AttributeError - with pytest.raises(Exception): + with pytest.raises(Exception): # noqa: B017 x[...] # exception from FSStore.__getitem__ getting AttributeError - with pytest.raises(Exception): + with pytest.raises(Exception): # noqa: B017 y[...]