diff --git a/.github/workflows/test-pypi-release.yml b/.github/workflows/test-pypi-release.yml new file mode 100644 index 0000000000..a1f5fdb3ec --- /dev/null +++ b/.github/workflows/test-pypi-release.yml @@ -0,0 +1,105 @@ +--- +name: Test PyPI Release + +on: + workflow_dispatch: + push: + tags: + - 'v*' + - 'test-release-*' + +permissions: + contents: read + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + build_artifacts: + name: Build distribution files + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + with: + submodules: true + fetch-depth: 0 + + - uses: actions/setup-python@v6 + name: Install Python + with: + python-version: '3.11' + + - name: Install Hatch + uses: pypa/hatch@257e27e51a6a5616ed08a39a408a21c35c9931bc + with: + version: '1.16.5' + + - name: Build wheel and sdist + run: hatch build + + - uses: actions/upload-artifact@v7 + with: + name: distribution + path: dist + + test_testpypi_upload: + needs: build_artifacts + runs-on: ubuntu-latest + environment: + name: testpypi + url: https://test.pypi.org/p/zarr + + steps: + - uses: actions/download-artifact@v7 + with: + name: distribution + path: dist + + - name: List artifacts + run: ls -la dist/ + + - name: Publish package to TestPyPI + uses: pypa/gh-action-pypi-publish@v1.13.0 + with: + repository-url: https://test.pypi.org/legacy/ + password: ${{ secrets.TESTPYPI_API_TOKEN }} + + test_testpypi_install: + needs: test_testpypi_upload + runs-on: ubuntu-latest + strategy: + matrix: + python-version: ['3.9', '3.11'] + fail-fast: true + + steps: + - uses: actions/checkout@v6 + + - uses: actions/setup-python@v6 + with: + python-version: ${{ matrix.python-version }} + + - name: Install from TestPyPI + run: | + python -m pip install --index-url https://test.pypi.org/simple/ \ + --extra-index-url https://pypi.org/simple/ \ + zarr + + - name: Smoke test + run: | + python -c " + import zarr + print(f'zarr version: {zarr.__version__}') + print(f'zarr location: {zarr.__file__}') + + # Basic functionality test + store = zarr.MemoryStore() + root = zarr.open_group(store=store, mode='w') + array = root.create_dataset('test', data=[1, 2, 3]) + assert len(array) == 3, 'Failed to create/read dataset' + print('✓ Basic zarr operations work correctly') + " + + - name: Print success message + run: echo "✓ TestPyPI installation and smoke tests passed!" diff --git a/changes/3798.feature.md b/changes/3798.feature.md new file mode 100644 index 0000000000..43c7e386ce --- /dev/null +++ b/changes/3798.feature.md @@ -0,0 +1 @@ +Add GitHub Actions workflow to test distributions on TestPyPI before releases. This workflow validates the package build process, ensures uploads work correctly, and confirms installation from TestPyPI succeeds across multiple Python versions, catching packaging issues early. diff --git a/src/zarr/core/array.py b/src/zarr/core/array.py index b82c77fa9c..330c62aab1 100644 --- a/src/zarr/core/array.py +++ b/src/zarr/core/array.py @@ -1039,6 +1039,38 @@ def shape(self) -> tuple[int, ...]: """ return self.metadata.shape + def __len__(self) -> int: + """Return the length of the first dimension. + + Matches numpy behavior: returns shape[0] for dimensioned arrays, + raises TypeError for 0-dimensional arrays. + + Returns + ------- + int + The size of the first dimension. + + Raises + ------ + TypeError + If the array is 0-dimensional (empty shape). + + Examples + -------- + >>> import zarr + >>> a = zarr.zeros((5, 10)) + >>> len(a) + 5 + >>> b = zarr.zeros(()) + >>> len(b) # doctest: +SKIP + Traceback (most recent call last): + ... + TypeError: len() of unsized object + """ + if self.ndim == 0: + raise TypeError("len() of unsized object") + return self.shape[0] + @property def chunks(self) -> tuple[int, ...]: """Returns the chunk shape of the Array. @@ -2263,6 +2295,36 @@ def shape(self, value: tuple[int, ...]) -> None: """Sets the shape of the array by calling resize.""" self.resize(value) + def __len__(self) -> int: + """Return the length of the first dimension. + + Matches numpy behavior: returns shape[0] for dimensioned arrays, + raises TypeError for 0-dimensional arrays. + + Returns + ------- + int + The size of the first dimension. + + Raises + ------ + TypeError + If the array is 0-dimensional (empty shape). + + Examples + -------- + >>> import zarr + >>> a = zarr.zeros((5, 10)) + >>> len(a) + 5 + >>> b = zarr.zeros(()) + >>> len(b) # doctest: +SKIP + Traceback (most recent call last): + ... + TypeError: len() of unsized object + """ + return self.async_array.__len__() + @property def chunks(self) -> tuple[int, ...]: """Returns a tuple of integers describing the length of each dimension of a chunk of the array. diff --git a/tests/test_array.py b/tests/test_array.py index 5b85c6ba1d..a8d912b48c 100644 --- a/tests/test_array.py +++ b/tests/test_array.py @@ -2299,3 +2299,31 @@ def test_with_config_polymorphism() -> None: arr_source_config_dict = arr.with_config(source_config_dict) assert arr_source_config.config == arr_source_config_dict.config + + +@pytest.mark.parametrize("shape", [(10,), (5, 10), (3, 4, 5), (2, 3, 4, 5)]) +def test_array_len_dimensioned(shape: tuple[int, ...]) -> None: + """Test __len__ for dimensioned arrays returns shape[0].""" + arr = zarr.create_array({}, shape=shape, dtype="uint8") + assert len(arr) == shape[0] + + +@pytest.mark.parametrize("shape", [(10,), (5, 10), (3, 4, 5)]) +async def test_array_len_dimensioned_async(shape: tuple[int, ...]) -> None: + """Test __len__ for async dimensioned arrays returns shape[0].""" + arr = await AsyncArray.create({}, shape=shape, dtype="uint8") + assert len(arr) == shape[0] + + +def test_array_len_0d_raises() -> None: + """Test __len__ raises TypeError for 0-dimensional arrays.""" + arr = zarr.create_array({}, shape=(), dtype="uint8") + with pytest.raises(TypeError, match="len\\(\\) of unsized object"): + len(arr) + + +async def test_array_len_0d_raises_async() -> None: + """Test __len__ raises TypeError for async 0-dimensional arrays.""" + arr = await AsyncArray.create({}, shape=(), dtype="uint8") + with pytest.raises(TypeError, match="len\\(\\) of unsized object"): + len(arr)