Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
105 changes: 105 additions & 0 deletions .github/workflows/test-pypi-release.yml
Original file line number Diff line number Diff line change
@@ -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!"
1 change: 1 addition & 0 deletions changes/3798.feature.md
Original file line number Diff line number Diff line change
@@ -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.
62 changes: 62 additions & 0 deletions src/zarr/core/array.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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.
Expand Down
28 changes: 28 additions & 0 deletions tests/test_array.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Loading