diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 971fc415af..3a62083f72 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -153,3 +153,20 @@ jobs: run: exit 1 - name: Success run: echo Success! + + pylint: + name: Pylint + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v5 + - name: Set up Python + uses: actions/setup-python@v6 + with: + python-version: '3.*' + - name: Install Pylint + run: | + python -m pip install --upgrade pip + pip install pylint + - name: Analyse the code with Pylint + run: | + pylint . diff --git a/pyproject.toml b/pyproject.toml index 7f14971396..63a078fa54 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -452,3 +452,57 @@ ignore-words-list = "astroid" [project.entry-points.pytest11] zarr = "zarr.testing" + +[tool.pylint] +disable = [ + "E0401", # import-error + "E0606", # possibly-used-before-assignment # FIXME + "E1123", # unexpected-keyword-arg # FIXME + "E1125", # missing-kwoa # FIXME + "E1136", # unsubscriptable-object + "W0104", # pointless-statement + "W0105", # pointless-string-statement + "W0106", # expression-not-assigned + "W0212", # protected-access + "W0223", # abstract-method # FIXME + "W0246", # useless-parent-delegation # FIXME + "W0511", # fixme + "W0602", # global-variable-not-assigned + "W0603", # global-statement + "W0611", # unused-import # FIXME + "W0613", # unused-argument + "W0621", # redefined-outer-name + "W0622", # redefined-builtin + "W0718", # broad-exception-caught + "W1510", # subprocess-run-check + "C0103", # invalid-name + "C0104", # disallowed-name + "C0105", # typevar-name-incorrect-variance + "C0114", # missing-module-docstring + "C0115", # missing-class-docstring + "C0116", # missing-function-docstring + "C0123", # unidiomatic-typecheck + "C0301", # line-too-long + "C0302", # too-many-lines + "C0411", # wrong-import-order + "C0412", # ungrouped-imports + "C0413", # wrong-import-position + "C0415", # import-outside-toplevel + "C1803", # use-implicit-booleaness-not-comparison + "R0124", # comparison-with-itself + "R0801", # duplicate-code + "R0902", # too-many-instance-attributes + "R0903", # too-few-public-methods + "R0904", # too-many-public-methods + "R0911", # too-many-return-statements + "R0912", # too-many-branches + "R0913", # too-many-arguments + "R0914", # too-many-locals + "R0915", # too-many-statements + "R0917", # too-many-positional-arguments + "R1702", # too-many-nested-blocks + "R1705", # no-else-return + "R1720", # no-else-raise + "R1728", # consider-using-generator + "R1732", # consider-using-with # FIXME +] diff --git a/src/zarr/abc/codec.py b/src/zarr/abc/codec.py index d41c457b4e..0d171a7556 100644 --- a/src/zarr/abc/codec.py +++ b/src/zarr/abc/codec.py @@ -86,7 +86,6 @@ def compute_encoded_size(self, input_byte_length: int, chunk_spec: ArraySpec) -> ------- int """ - ... def resolve_metadata(self, chunk_spec: ArraySpec) -> ArraySpec: """Computed the spec of the chunk after it has been encoded by the codec. @@ -289,7 +288,6 @@ def evolve_from_array_spec(self, array_spec: ArraySpec) -> Self: ------- Self """ - ... @classmethod @abstractmethod @@ -304,7 +302,6 @@ def from_codecs(cls, codecs: Iterable[Codec]) -> Self: ------- Self """ - ... @classmethod def from_array_metadata_and_store(cls, array_metadata: ArrayMetadata, store: Store) -> Self: @@ -353,7 +350,6 @@ def validate( chunk_grid : ChunkGrid The array chunk grid """ - ... @abstractmethod def compute_encoded_size(self, byte_length: int, array_spec: ArraySpec) -> int: @@ -369,7 +365,6 @@ def compute_encoded_size(self, byte_length: int, array_spec: ArraySpec) -> int: ------- int """ - ... @abstractmethod async def decode( @@ -388,7 +383,6 @@ async def decode( ------- Iterable[NDBuffer | None] """ - ... @abstractmethod async def encode( @@ -407,7 +401,6 @@ async def encode( ------- Iterable[Buffer | None] """ - ... @abstractmethod async def read( @@ -434,7 +427,6 @@ async def read( out : NDBuffer """ - ... @abstractmethod async def write( @@ -457,7 +449,6 @@ async def write( The chunk spec contains information about the chunk. value : NDBuffer """ - ... async def _batching_helper( diff --git a/src/zarr/abc/numcodec.py b/src/zarr/abc/numcodec.py index 76eac1d898..9739272fb9 100644 --- a/src/zarr/abc/numcodec.py +++ b/src/zarr/abc/numcodec.py @@ -26,7 +26,6 @@ def encode(self, buf: Any) -> Any: enc: Any Encoded data. """ - ... def decode(self, buf: Any, out: Any | None = None) -> Any: """ @@ -45,14 +44,12 @@ def decode(self, buf: Any, out: Any | None = None) -> Any: dec : Any Decoded data. """ - ... def get_config(self) -> Any: """ Return a JSON-serializable configuration dictionary for this codec. Must include an ``'id'`` field with the codec identifier. """ - ... @classmethod def from_config(cls, config: Any) -> Self: @@ -64,7 +61,6 @@ def from_config(cls, config: Any) -> Self: config : Any A configuration dictionary for this codec. """ - ... def _is_numcodec_cls(obj: object) -> TypeGuard[type[Numcodec]]: diff --git a/src/zarr/abc/store.py b/src/zarr/abc/store.py index 4b3edf78d1..4449d70ce6 100644 --- a/src/zarr/abc/store.py +++ b/src/zarr/abc/store.py @@ -178,7 +178,6 @@ def _check_writable(self) -> None: @abstractmethod def __eq__(self, value: object) -> bool: """Equality comparison.""" - ... @abstractmethod async def get( @@ -204,7 +203,6 @@ async def get( ------- Buffer """ - ... @abstractmethod async def get_partial_values( @@ -225,7 +223,6 @@ async def get_partial_values( ------- list of values, in the order of the key_ranges, may contain null/none for missing keys """ - ... @abstractmethod async def exists(self, key: str) -> bool: @@ -239,13 +236,11 @@ async def exists(self, key: str) -> bool: ------- bool """ - ... @property @abstractmethod def supports_writes(self) -> bool: """Does the store support writes?""" - ... @abstractmethod async def set(self, key: str, value: Buffer) -> None: @@ -256,7 +251,6 @@ async def set(self, key: str, value: Buffer) -> None: key : str value : Buffer """ - ... async def set_if_not_exists(self, key: str, value: Buffer) -> None: """ @@ -296,7 +290,6 @@ def supports_consolidated_metadata(self) -> bool: @abstractmethod def supports_deletes(self) -> bool: """Does the store support deletes?""" - ... @abstractmethod async def delete(self, key: str) -> None: @@ -306,7 +299,6 @@ async def delete(self, key: str) -> None: ---------- key : str """ - ... @property def supports_partial_writes(self) -> Literal[False]: @@ -320,7 +312,6 @@ def supports_partial_writes(self) -> Literal[False]: @abstractmethod def supports_listing(self) -> bool: """Does the store support listing?""" - ... @abstractmethod def list(self) -> AsyncIterator[str]: diff --git a/src/zarr/codecs/numcodecs/_codecs.py b/src/zarr/codecs/numcodecs/_codecs.py index 4a3d88a84f..a58f990c6b 100644 --- a/src/zarr/codecs/numcodecs/_codecs.py +++ b/src/zarr/codecs/numcodecs/_codecs.py @@ -137,9 +137,6 @@ def __repr__(self) -> str: class _NumcodecsBytesBytesCodec(_NumcodecsCodec, BytesBytesCodec): - def __init__(self, **codec_config: JSON) -> None: - super().__init__(**codec_config) - async def _decode_single(self, chunk_data: Buffer, chunk_spec: ArraySpec) -> Buffer: return await asyncio.to_thread( as_numpy_array_wrapper, @@ -159,9 +156,6 @@ async def _encode_single(self, chunk_data: Buffer, chunk_spec: ArraySpec) -> Buf class _NumcodecsArrayArrayCodec(_NumcodecsCodec, ArrayArrayCodec): - def __init__(self, **codec_config: JSON) -> None: - super().__init__(**codec_config) - async def _decode_single(self, chunk_data: NDBuffer, chunk_spec: ArraySpec) -> NDBuffer: chunk_ndarray = chunk_data.as_ndarray_like() out = await asyncio.to_thread(self._codec.decode, chunk_ndarray) @@ -174,9 +168,6 @@ async def _encode_single(self, chunk_data: NDBuffer, chunk_spec: ArraySpec) -> N class _NumcodecsArrayBytesCodec(_NumcodecsCodec, ArrayBytesCodec): - def __init__(self, **codec_config: JSON) -> None: - super().__init__(**codec_config) - async def _decode_single(self, chunk_data: Buffer, chunk_spec: ArraySpec) -> NDBuffer: chunk_bytes = chunk_data.to_bytes() out = await asyncio.to_thread(self._codec.decode, chunk_bytes) @@ -253,9 +244,6 @@ def evolve_from_array_spec(self, array_spec: ArraySpec) -> FixedScaleOffset: class Quantize(_NumcodecsArrayArrayCodec, codec_name="quantize"): - def __init__(self, **codec_config: JSON) -> None: - super().__init__(**codec_config) - def evolve_from_array_spec(self, array_spec: ArraySpec) -> Quantize: if self.codec_config.get("dtype") is None: dtype = array_spec.dtype.to_native_dtype() diff --git a/src/zarr/core/buffer/core.py b/src/zarr/core/buffer/core.py index 189916dc91..393f179534 100644 --- a/src/zarr/core/buffer/core.py +++ b/src/zarr/core/buffer/core.py @@ -253,7 +253,6 @@ def as_numpy_array(self) -> npt.NDArray[Any]: ------- NumPy array of this buffer (might be a data copy) """ - ... def as_buffer_like(self) -> BytesLike: """Returns the buffer as an object that implements the Python buffer protocol. @@ -296,7 +295,6 @@ def __len__(self) -> int: @abstractmethod def __add__(self, other: Buffer) -> Self: """Concatenate two buffers""" - ... def __eq__(self, other: object) -> bool: # Another Buffer class can override this to choose a more efficient path @@ -469,7 +467,6 @@ def as_numpy_array(self) -> npt.NDArray[Any]: ------- NumPy array of this buffer (might be a data copy) """ - ... def as_scalar(self) -> ScalarType: """Returns the buffer as a scalar value""" diff --git a/src/zarr/core/dtype/common.py b/src/zarr/core/dtype/common.py index 652b5fdbe3..dd6272abd1 100644 --- a/src/zarr/core/dtype/common.py +++ b/src/zarr/core/dtype/common.py @@ -80,7 +80,7 @@ def check_structured_dtype_v2_inner(data: object) -> TypeGuard[StructuredName_V2 return False if len(data) != 2: return False - if not (isinstance(data[0], str)): + if not isinstance(data[0], str): return False if isinstance(data[-1], str): return True diff --git a/src/zarr/core/group.py b/src/zarr/core/group.py index 26aed4fd60..5f165004de 100644 --- a/src/zarr/core/group.py +++ b/src/zarr/core/group.py @@ -3208,11 +3208,13 @@ async def create_hierarchy( nodes_parsed = _parse_hierarchy_dict(data=nodes_normed_keys) redundant_implicit_groups = [] - # empty hierarchies should be a no-op - if len(nodes_parsed) > 0: + try: # figure out which zarr format we are using zarr_format = next(iter(nodes_parsed.values())).zarr_format - + except StopIteration: + # empty hierarchies should be a no-op + pass + else: # check which implicit groups will require materialization implicit_group_keys = tuple( filter(lambda k: isinstance(nodes_parsed[k], ImplicitGroupMarker), nodes_parsed) diff --git a/src/zarr/core/metadata/v2.py b/src/zarr/core/metadata/v2.py index 3204543426..dd3ce32194 100644 --- a/src/zarr/core/metadata/v2.py +++ b/src/zarr/core/metadata/v2.py @@ -265,11 +265,10 @@ def parse_filters(data: object) -> tuple[Numcodec, ...] | None: """ Parse a potential tuple of filters """ - out: list[Numcodec] = [] - if data is None: return data if isinstance(data, Iterable): + out: list[Numcodec] = [] for idx, val in enumerate(data): if _is_numcodec(val): out.append(val) diff --git a/src/zarr/storage/_zip.py b/src/zarr/storage/_zip.py index 72bf9e335a..9fb96bbce6 100644 --- a/src/zarr/storage/_zip.py +++ b/src/zarr/storage/_zip.py @@ -84,6 +84,7 @@ def __init__( assert isinstance(path, Path) self.path = path # root? + self._is_open = False self._zmode = mode self.compression = compression self.allowZip64 = allowZip64 @@ -123,6 +124,7 @@ def close(self) -> None: super().close() with self._lock: self._zf.close() + self._is_open = False async def clear(self) -> None: # docstring inherited diff --git a/src/zarr/testing/stateful.py b/src/zarr/testing/stateful.py index c363c13983..81a48f879b 100644 --- a/src/zarr/testing/stateful.py +++ b/src/zarr/testing/stateful.py @@ -161,7 +161,7 @@ def add_array( @with_frequency(0.25) def clear(self) -> None: note("clearing") - import zarr + import zarr # pylint: disable=reimported self._sync(self.store.clear()) self._sync(self.model.clear()) diff --git a/src/zarr/testing/store.py b/src/zarr/testing/store.py index ad3b80da41..befefb76bd 100644 --- a/src/zarr/testing/store.py +++ b/src/zarr/testing/store.py @@ -45,7 +45,6 @@ async def set(self, store: S, key: str, value: Buffer) -> None: This should not use any store methods. Bypassing the store methods allows them to be tested. """ - ... @abstractmethod async def get(self, store: S, key: str) -> Buffer: @@ -54,13 +53,11 @@ async def get(self, store: S, key: str) -> Buffer: This should not use any store methods. Bypassing the store methods allows them to be tested. """ - ... @abstractmethod @pytest.fixture def store_kwargs(self, *args: Any, **kwargs: Any) -> dict[str, Any]: """Kwargs for instantiating a store""" - ... @abstractmethod def test_store_repr(self, store: S) -> None: ... diff --git a/tests/conftest.py b/tests/conftest.py index 63c8950cff..d368178631 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -121,7 +121,7 @@ async def store2(request: pytest.FixtureRequest, tmpdir: LEGACY_PATH) -> Store: def sync_store(request: pytest.FixtureRequest, tmp_path: LEGACY_PATH) -> Store: result = sync(parse_store(request.param, str(tmp_path))) if not isinstance(result, Store): - raise TypeError("Wrong store class returned by test fixture! got " + result + " instead") + raise TypeError(f"Wrong store class returned by test fixture! got {result} instead") return result diff --git a/tests/test_array.py b/tests/test_array.py index 5219616739..e22d534369 100644 --- a/tests/test_array.py +++ b/tests/test_array.py @@ -66,7 +66,6 @@ from zarr.core.group import AsyncGroup from zarr.core.indexing import BasicIndexer, _iter_grid, _iter_regions from zarr.core.metadata.v2 import ArrayV2Metadata -from zarr.core.metadata.v3 import ArrayV3Metadata from zarr.core.sync import sync from zarr.errors import ( ContainsArrayError, diff --git a/tests/test_codec_entrypoints.py b/tests/test_codec_entrypoints.py index fc7b79fe54..69cd0a1577 100644 --- a/tests/test_codec_entrypoints.py +++ b/tests/test_codec_entrypoints.py @@ -7,7 +7,7 @@ @pytest.mark.usefixtures("set_path") @pytest.mark.parametrize("codec_name", ["TestEntrypointCodec", "TestEntrypointGroup.Codec"]) def test_entrypoint_codec(codec_name: str) -> None: - config.set({"codecs.test": "package_with_entrypoint." + codec_name}) + config.set({"codecs.test": f"package_with_entrypoint.{codec_name}"}) cls_test = zarr.registry.get_codec_class("test") assert cls_test.__qualname__ == codec_name @@ -24,7 +24,7 @@ def test_entrypoint_pipeline() -> None: def test_entrypoint_buffer(buffer_name: str) -> None: config.set( { - "buffer": "package_with_entrypoint." + buffer_name, + "buffer": f"package_with_entrypoint.{buffer_name}", "ndbuffer": "package_with_entrypoint.TestEntrypointNDBuffer", } ) diff --git a/tests/test_properties.py b/tests/test_properties.py index 705cfd1b59..8812229812 100644 --- a/tests/test_properties.py +++ b/tests/test_properties.py @@ -10,6 +10,8 @@ pytest.importorskip("hypothesis") +from itertools import starmap + import hypothesis.extra.numpy as npst import hypothesis.strategies as st from hypothesis import assume, given, settings @@ -60,7 +62,7 @@ def deep_equal(a: Any, b: Any) -> bool: if isinstance(a, np.ndarray) and isinstance(b, np.ndarray): if a.shape != b.shape: return False - return all(deep_equal(x, y) for x, y in zip(a.flat, b.flat, strict=False)) + return all(starmap(deep_equal, zip(a.flat, b.flat, strict=False))) if isinstance(a, dict) and isinstance(b, dict): if set(a.keys()) != set(b.keys()): @@ -70,7 +72,7 @@ def deep_equal(a: Any, b: Any) -> bool: if isinstance(a, (list, tuple)) and isinstance(b, (list, tuple)): if len(a) != len(b): return False - return all(deep_equal(x, y) for x, y in zip(a, b, strict=False)) + return all(starmap(deep_equal, zip(a, b, strict=False))) return a == b diff --git a/tests/test_v2.py b/tests/test_v2.py index b223e022c6..d66c69ebb6 100644 --- a/tests/test_v2.py +++ b/tests/test_v2.py @@ -144,13 +144,13 @@ def test_create_array_defaults(store: Store) -> None: assert isinstance(g, Group) arr = g.create_array("one", dtype="i8", shape=(1,), chunks=(1,), compressor=None) assert arr._async_array.compressor is None - assert not (arr.filters) + assert not arr.filters arr = g.create_array("two", dtype="i8", shape=(1,), chunks=(1,)) assert arr._async_array.compressor is not None - assert not (arr.filters) + assert not arr.filters arr = g.create_array("three", dtype="i8", shape=(1,), chunks=(1,), compressor=Zstd()) assert arr._async_array.compressor is not None - assert not (arr.filters) + assert not arr.filters with pytest.raises(ValueError): g.create_array( "four", dtype="i8", shape=(1,), chunks=(1,), compressor=None, compressors=None