From cc4c3f53256719698b7731a9702f22f17d7eeff8 Mon Sep 17 00:00:00 2001 From: Matthew Flamm Date: Mon, 18 Jul 2022 17:25:43 -0400 Subject: [PATCH 01/27] make MultiBlock __getitem__ and __setitem__ closer to list (and dict) --- pyvista/core/composite.py | 106 ++++++++++++++++++++----------- pyvista/core/filters/data_set.py | 65 +++++++++++-------- pyvista/utilities/fileio.py | 2 +- tests/plotting/test_plotting.py | 2 +- tests/test_composite.py | 50 +++++++-------- 5 files changed, 133 insertions(+), 92 deletions(-) diff --git a/pyvista/core/composite.py b/pyvista/core/composite.py index b6dc30217a..44cc73607e 100644 --- a/pyvista/core/composite.py +++ b/pyvista/core/composite.py @@ -6,7 +6,7 @@ import collections.abc import logging import pathlib -from typing import Any, List, Optional, Tuple, Union, cast +from typing import Any, List, Optional, Sequence, Tuple, Union, cast import numpy as np @@ -99,10 +99,8 @@ def __init__(self, *args, **kwargs) -> None: elif isinstance(args[0], (str, pathlib.Path)): self._from_file(args[0], **kwargs) elif isinstance(args[0], dict): - idx = 0 for key, block in args[0].items(): - self[idx, key] = block - idx += 1 + self.append(block, key) else: raise TypeError(f'Type {type(args[0])} is not supported by pyvista.MultiBlock') @@ -292,21 +290,16 @@ def __getitem__(self, index: Union[int, str]) -> Optional['MultiBlock']: if isinstance(index, slice): multi = MultiBlock() for i in range(self.n_blocks)[index]: - multi[-1, self.get_block_name(i)] = self[i] - return multi - elif isinstance(index, (list, tuple, np.ndarray)): - multi = MultiBlock() - for i in index: - name = i if isinstance(i, str) else self.get_block_name(i) - multi[-1, name] = self[i] # type: ignore + multi.append(self[i], self.get_block_name(i)) return multi elif isinstance(index, str): index = self.get_index_by_name(index) ############################ + if index < -self.n_blocks or index >= self.n_blocks: + raise IndexError(f'index ({index}) out of range for this dataset.') if index < 0: index = self.n_blocks + index - if index < 0 or index >= self.n_blocks: - raise IndexError(f'index ({index}) out of range for this dataset.') + data = self.GetBlock(index) if data is None: return data @@ -314,7 +307,7 @@ def __getitem__(self, index: Union[int, str]) -> Optional['MultiBlock']: data = wrap(data) return data - def append(self, dataset: DataSet): + def append(self, dataset: DataSet, name: Optional[str] = None): """Add a data set to the next block index. Parameters @@ -322,21 +315,32 @@ def append(self, dataset: DataSet): dataset : pyvista.DataSet Dataset to append to this multi-block. + name : str, optional + Block name to give to dataset. A default name is given + depending on the block index. + Examples -------- >>> import pyvista as pv + >>> from pyvista import examples >>> data = {"cube": pv.Cube(), "sphere": pv.Sphere(center=(2, 2, 0))} >>> blocks = pv.MultiBlock(data) >>> blocks.append(pv.Cone()) >>> len(blocks) 3 + >>> blocks.append(examples.load_uniform(), "uniform") + >>> blocks.keys() + ['cube', 'sphere', 'Block-02', 'uniform'] """ index = self.n_blocks # note off by one so use as index # always wrap since we may need to reference the VTK memory address if not pyvista.is_pyvista_dataset(dataset): dataset = pyvista.wrap(dataset) + self.n_blocks += 1 self[index] = dataset + # No overwrite if name is None + self.set_block_name(index, name) def get(self, index: Union[int, str]) -> Optional['MultiBlock']: """Get a block by its index or name. @@ -356,7 +360,7 @@ def get(self, index: Union[int, str]) -> Optional['MultiBlock']: """ return self[index] - def set_block_name(self, index: int, name: str): + def set_block_name(self, index: int, name: Optional[str]): """Set a block's string name at the specified index. Parameters @@ -432,7 +436,11 @@ def keys(self) -> List[Optional[str]]: def _ipython_key_completions_(self) -> List[Optional[str]]: return self.keys() - def __setitem__(self, index: Union[Tuple[int, Optional[str]], int, str], data: DataSet): + def __setitem__( + self, + index: Union[int, str, slice], + data: Union[DataSet, Sequence[DataSet], Tuple[str, DataSet]], + ): """Set a block with a VTK data object. To set the name simultaneously, pass a string name as the 2nd index. @@ -441,43 +449,67 @@ def __setitem__(self, index: Union[Tuple[int, Optional[str]], int, str], data: D ------- >>> import pyvista >>> multi = pyvista.MultiBlock() - >>> multi[0] = pyvista.PolyData() - >>> multi[1, 'foo'] = pyvista.UnstructuredGrid() + >>> multi.append(pyvista.PolyData()) + >>> multi[0] = pyvista.UnstructuredGrid() + >>> multi.append(pyvista.PolyData(), 'poly') + >>> multi[1] = ('foo', pyvista.UnstructuredGrid()) + >>> multi.keys() + ['Block-00', 'foo'] >>> multi['bar'] = pyvista.PolyData() >>> multi.n_blocks 3 """ + if isinstance(index, str) and isinstance(data, tuple): + raise TypeError(f"Cannot set key {index} with a different string key from {data}") + i: int = 0 name: Optional[str] = None - if isinstance(index, (np.ndarray, collections.abc.Sequence)) and not isinstance(index, str): - i, name = index[0], index[1] - elif isinstance(index, str): + if isinstance(index, str): try: i = self.get_index_by_name(index) except KeyError: - i = -1 + # data cannot be a tuple here + if isinstance(data, collections.abc.Sequence): + raise TypeError(f"Cannot set key {index} with the sequence {data}") + self.append(data, index) + return name = index - else: - i, name = cast(int, index), None + elif isinstance(index, slice): + if isinstance(data, tuple): + raise TypeError(f"Cannot set the slice {slice} with a tuple {tuple}") + for i, d in zip(range(self.n_blocks)[index], data): + self[i] = d + return + + # data, i, and name are a single value now + i = cast(int, index) + + if isinstance(data, tuple): + if not len(data) == 2: + raise ValueError(f"Data {data} must be a length 2 tuple of form (DataSet, str)") + name, data = data + if data is not None and not is_pyvista_dataset(data): data = wrap(data) + data = cast(pyvista.DataSet, data) - if i == -1: - self.append(data) - i = self.n_blocks - 1 - else: - # this is the only spot in the class where we actually add - # data to the MultiBlock + if i < -self.n_blocks or i >= self.n_blocks: + raise IndexError(f'index ({i}) out of range for this dataset.') + if i < 0: + i = self.n_blocks + i + + # this is the only spot in the class where we actually add + # data to the MultiBlock - # check if we are overwriting a block - existing_dataset = self.GetBlock(i) - if existing_dataset is not None: - self._remove_ref(i) + # check if we are overwriting a block + existing_dataset = self.GetBlock(i) + if existing_dataset is not None: + self._remove_ref(i) - self.SetBlock(i, data) - if data is not None: - self._refs[data.memory_address] = data + self.SetBlock(i, data) + if data is not None: + self._refs[data.memory_address] = data if name is None: name = f'Block-{i:02}' diff --git a/pyvista/core/filters/data_set.py b/pyvista/core/filters/data_set.py index fe4feb20ff..eccb5f4bd3 100644 --- a/pyvista/core/filters/data_set.py +++ b/pyvista/core/filters/data_set.py @@ -641,27 +641,38 @@ def slice_orthogonal( output = pyvista.MultiBlock() if isinstance(self, pyvista.MultiBlock): for i in range(self.n_blocks): - output[i] = self[i].slice_orthogonal( - x=x, y=y, z=z, generate_triangles=generate_triangles, contour=contour + output.append( + self[i].slice_orthogonal( + x=x, y=y, z=z, generate_triangles=generate_triangles, contour=contour + ) ) return output - output[0, 'YZ'] = self.slice( - normal='x', - origin=[x, y, z], - generate_triangles=generate_triangles, - progress_bar=progress_bar, + output.append( + self.slice( + normal='x', + origin=[x, y, z], + generate_triangles=generate_triangles, + progress_bar=progress_bar, + ), + 'YZ', ) - output[1, 'XZ'] = self.slice( - normal='y', - origin=[x, y, z], - generate_triangles=generate_triangles, - progress_bar=progress_bar, + output.append( + self.slice( + normal='y', + origin=[x, y, z], + generate_triangles=generate_triangles, + progress_bar=progress_bar, + ), + 'XZ', ) - output[2, 'XY'] = self.slice( - normal='z', - origin=[x, y, z], - generate_triangles=generate_triangles, - progress_bar=progress_bar, + output.append( + self.slice( + normal='z', + origin=[x, y, z], + generate_triangles=generate_triangles, + progress_bar=progress_bar, + ), + 'XY', ) return output @@ -767,14 +778,16 @@ def slice_along_axis( output = pyvista.MultiBlock() if isinstance(self, pyvista.MultiBlock): for i in range(self.n_blocks): - output[i] = self[i].slice_along_axis( - n=n, - axis=ax_label, - tolerance=tolerance, - generate_triangles=generate_triangles, - contour=contour, - bounds=bounds, - center=center, + output.append( + self[i].slice_along_axis( + n=n, + axis=ax_label, + tolerance=tolerance, + generate_triangles=generate_triangles, + contour=contour, + bounds=bounds, + center=center, + ) ) return output for i in range(n): @@ -787,7 +800,7 @@ def slice_along_axis( contour=contour, progress_bar=progress_bar, ) - output[i, f'slice{i}'] = slc + output.append(slc, f'slice{i}') return output def slice_along_line(self, line, generate_triangles=False, contour=False, progress_bar=False): diff --git a/pyvista/utilities/fileio.py b/pyvista/utilities/fileio.py index f8290197e1..ea33186dd6 100644 --- a/pyvista/utilities/fileio.py +++ b/pyvista/utilities/fileio.py @@ -152,7 +152,7 @@ def read(filename, attrs=None, force_ext=None, file_format=None, progress_bar=Fa name = os.path.basename(str(each)) else: name = None - multi[-1, name] = read(each, attrs=attrs, file_format=file_format) + multi.append(read(each, attrs=attrs, file_format=file_format), name) return multi filename = os.path.abspath(os.path.expanduser(str(filename))) if not os.path.isfile(filename): diff --git a/tests/plotting/test_plotting.py b/tests/plotting/test_plotting.py index 986ace3b3b..93858efef6 100644 --- a/tests/plotting/test_plotting.py +++ b/tests/plotting/test_plotting.py @@ -1165,7 +1165,7 @@ def test_multi_block_plot(): uni.cell_data.set_array(arr, 'Random Data') multi.append(uni) # And now add a data set without the desired array and a NULL component - multi[3] = examples.load_airplane() + multi.append(examples.load_airplane()) with pytest.raises(KeyError): # The scalars are not available in all datasets so raises KeyError multi.plot(scalars='Random Data', multi_colors=True) diff --git a/tests/test_composite.py b/tests/test_composite.py index 8663a06722..96966b0142 100644 --- a/tests/test_composite.py +++ b/tests/test_composite.py @@ -123,7 +123,7 @@ def test_multi_block_set_get_ers(): assert multi.n_blocks == 6 # Check pyvista side registered it # Add data to the MultiBlock data = ex.load_rectilinear() - multi[1, 'rect'] = data + multi[1] = ('rect', data) # Make sure number of blocks is constant assert multi.n_blocks == 6 # Check content @@ -164,10 +164,11 @@ def test_multi_block_set_get_ers(): def test_multi_block_clean(rectilinear, uniform, ant): # now test a clean of the null values multi = MultiBlock() - multi[1, 'rect'] = rectilinear - multi[2, 'empty'] = PolyData() - multi[3, 'mempty'] = MultiBlock() - multi[5, 'uni'] = uniform + multi.n_blocks = 6 + multi[1] = ('rect', rectilinear) + multi[2] = ('empty', PolyData()) + multi[3] = ('mempty', MultiBlock()) + multi[5] = ('uni', uniform) # perform the clean to remove all Null elements multi.clean() assert multi.n_blocks == 2 @@ -178,11 +179,13 @@ def test_multi_block_clean(rectilinear, uniform, ant): assert multi.get_block_name(1) == 'uni' # Test a nested data struct foo = MultiBlock() + foo.n_blocks = 4 foo[3] = ant assert foo.n_blocks == 4 multi = MultiBlock() - multi[1, 'rect'] = rectilinear - multi[5, 'multi'] = foo + multi.n_blocks = 6 + multi[1] = ('rect', rectilinear) + multi[5] = ('multi', foo) # perform the clean to remove all Null elements assert multi.n_blocks == 6 multi.clean() @@ -344,6 +347,14 @@ def test_multi_block_negative_index(ant, sphere, uniform, airplane, globe): with pytest.raises(IndexError): _ = multi[-6] + multi[-1] = ant + assert multi[4] == ant + multi[-5] = globe + assert multi[0] == globe + + with pytest.raises(IndexError): + multi[-6] = uniform + def test_multi_slice_index(ant, sphere, uniform, airplane, globe): multi = multi_from_datasets(ant, sphere, uniform, airplane, globe) @@ -365,6 +376,11 @@ def test_multi_slice_index(ant, sphere, uniform, airplane, globe): assert sub[i] is multi[j] assert sub.get_block_name(i) == multi.get_block_name(j) + sub = [airplane, globe] + multi[0:2] = sub + assert multi[0] is airplane + assert multi[1] is globe + def test_slice_defaults(ant, sphere, uniform, airplane, globe): multi = multi_from_datasets(ant, sphere, uniform, airplane, globe) @@ -386,26 +402,6 @@ def test_slice_negatives(ant, sphere, uniform, airplane, globe): assert multi[-1:-4:-2] == test_multi -def test_multi_block_list_index(ant, sphere, uniform, airplane, globe): - multi = multi_from_datasets(ant, sphere, uniform, airplane, globe) - # Now check everything - indices = [0, 3, 4] - sub = multi[indices] - assert len(sub) == len(indices) - for i, j in enumerate(indices): - assert id(sub[i]) == id(multi[j]) - assert sub.get_block_name(i) == multi.get_block_name(j) - # check list of key names - multi = MultiBlock() - multi["foo"] = pyvista.Sphere() - multi["goo"] = pyvista.Box() - multi["soo"] = pyvista.Cone() - indices = ["goo", "foo"] - sub = multi[indices] - assert len(sub) == len(indices) - assert isinstance(sub["foo"], PolyData) - - def test_multi_block_volume(ant, airplane, sphere, uniform): multi = multi_from_datasets(ant, sphere, uniform, airplane, None) vols = ant.volume + sphere.volume + uniform.volume + airplane.volume From 01174bfc1cb3bbe3c2758bd81d71259c73b3f2fb Mon Sep 17 00:00:00 2001 From: Matthew Flamm Date: Tue, 19 Jul 2022 15:58:09 -0400 Subject: [PATCH 02/27] update get method to be more dict-like --- pyvista/core/composite.py | 27 ++++++++++++++++++++++----- tests/test_composite.py | 3 +++ 2 files changed, 25 insertions(+), 5 deletions(-) diff --git a/pyvista/core/composite.py b/pyvista/core/composite.py index 44cc73607e..7a06be758d 100644 --- a/pyvista/core/composite.py +++ b/pyvista/core/composite.py @@ -342,23 +342,40 @@ def append(self, dataset: DataSet, name: Optional[str] = None): # No overwrite if name is None self.set_block_name(index, name) - def get(self, index: Union[int, str]) -> Optional['MultiBlock']: + def get(self, index: str, default: Optional[DataSet] = None) -> Optional[DataSet]: """Get a block by its index or name. If the name is non-unique then returns the first occurrence. + Returns ``default`` if name isn't in the dataset. Parameters ---------- - index : int or str + index : str Index or name of the dataset within the multiblock. + default : pyvista.DataSet, optional + Default to return if index is not in the multiblock. + Returns ------- - pyvista.DataSet - Dataset from the given index. + pyvista.DataSet or None + Dataset from the given index if it exists. + + Examples + -------- + >>> import pyvista as pv + >>> from pyvista import examples + >>> data = {"poly": pv.PolyData(), "uni": pv.UniformGrid()} + >>> blocks = pv.MultiBlock(data) + >>> blocks.get("poly") + PolyData ... + >>> blocks.get("cone") """ - return self[index] + try: + return self[index] + except KeyError: + return default def set_block_name(self, index: int, name: Optional[str]): """Set a block's string name at the specified index. diff --git a/tests/test_composite.py b/tests/test_composite.py index 96966b0142..78e93dd239 100644 --- a/tests/test_composite.py +++ b/tests/test_composite.py @@ -139,6 +139,9 @@ def test_multi_block_set_get_ers(): # Test get by name assert isinstance(multi['uni'], UniformGrid) assert isinstance(multi['rect'], RectilinearGrid) + assert isinstance(multi.get('uni'), UniformGrid) + assert multi.get('no key') is None + assert multi.get('no key', default=pyvista.Sphere()) == pyvista.Sphere() # Test the del operator del multi[0] assert multi.n_blocks == 5 From 641db6c4712bd03619e76a227b8fa45a6c7b09ba Mon Sep 17 00:00:00 2001 From: Matthew Flamm Date: Tue, 19 Jul 2022 17:41:13 -0400 Subject: [PATCH 03/27] more test coverage for errors --- tests/test_composite.py | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/tests/test_composite.py b/tests/test_composite.py index 78e93dd239..5f29c59432 100644 --- a/tests/test_composite.py +++ b/tests/test_composite.py @@ -159,9 +159,22 @@ def test_multi_block_set_get_ers(): assert multi.get_block_name(10) is None with pytest.raises(KeyError): _ = multi.get_index_by_name('foo') - # allow Sequence but not Iterable in setitem + + with pytest.raises(KeyError): + multi["not a key"] + with pytest.raises(TypeError): + data = multi[[0, 1]] + + with pytest.raises(TypeError): + multi[1, 'foo'] = data + with pytest.raises(ValueError): + multi[1] = (UniformGrid(),) + + with pytest.raises(TypeError): + multi["rect"] = ("another key", UniformGrid()) + with pytest.raises(TypeError): - multi[{1, 'foo'}] = data + multi["not a key"] = [UniformGrid(), PolyData()] def test_multi_block_clean(rectilinear, uniform, ant): From 38b0817975ce400546acf7a3fd57efd3992c4a74 Mon Sep 17 00:00:00 2001 From: Matthew Flamm Date: Tue, 19 Jul 2022 17:46:30 -0400 Subject: [PATCH 04/27] one more test for out of bound setting --- tests/test_composite.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tests/test_composite.py b/tests/test_composite.py index 5f29c59432..9e445751cd 100644 --- a/tests/test_composite.py +++ b/tests/test_composite.py @@ -160,6 +160,9 @@ def test_multi_block_set_get_ers(): with pytest.raises(KeyError): _ = multi.get_index_by_name('foo') + with pytest.raises(IndexError): + multi[3] = UniformGrid() + with pytest.raises(KeyError): multi["not a key"] with pytest.raises(TypeError): From 047a075b0cd4f7ffc54e9291feaaf5fef74d813f Mon Sep 17 00:00:00 2001 From: Matthew Flamm Date: Tue, 19 Jul 2022 20:56:02 -0400 Subject: [PATCH 05/27] more tests, fix setting with exiating key, --- pyvista/core/composite.py | 4 ++-- tests/test_composite.py | 11 ++++++++++- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/pyvista/core/composite.py b/pyvista/core/composite.py index 7a06be758d..f6873a07f3 100644 --- a/pyvista/core/composite.py +++ b/pyvista/core/composite.py @@ -498,10 +498,10 @@ def __setitem__( for i, d in zip(range(self.n_blocks)[index], data): self[i] = d return + else: + i = index # data, i, and name are a single value now - i = cast(int, index) - if isinstance(data, tuple): if not len(data) == 2: raise ValueError(f"Data {data} must be a length 2 tuple of form (DataSet, str)") diff --git a/tests/test_composite.py b/tests/test_composite.py index 9e445751cd..9863e62e92 100644 --- a/tests/test_composite.py +++ b/tests/test_composite.py @@ -157,11 +157,20 @@ def test_multi_block_set_get_ers(): assert isinstance(pop, RectilinearGrid) assert multi.n_blocks == 3 assert multi.get_block_name(10) is None + + multi["new key"] = pyvista.Sphere() + assert multi.n_blocks == 4 + assert multi[3] == pyvista.Sphere() + + multi["new key"] = pyvista.Cube() + assert multi.n_blocks == 4 + assert multi[3] == pyvista.Cube() + with pytest.raises(KeyError): _ = multi.get_index_by_name('foo') with pytest.raises(IndexError): - multi[3] = UniformGrid() + multi[4] = UniformGrid() with pytest.raises(KeyError): multi["not a key"] From 12a94aae03c6d6a5f105915babc2b4431c5f9189 Mon Sep 17 00:00:00 2001 From: MatthewFlamm <39341281+MatthewFlamm@users.noreply.github.com> Date: Sat, 23 Jul 2022 13:32:30 -0400 Subject: [PATCH 06/27] Fix append description Co-authored-by: Alex Kaszynski --- pyvista/core/composite.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyvista/core/composite.py b/pyvista/core/composite.py index f6873a07f3..01f6f76a9c 100644 --- a/pyvista/core/composite.py +++ b/pyvista/core/composite.py @@ -343,7 +343,7 @@ def append(self, dataset: DataSet, name: Optional[str] = None): self.set_block_name(index, name) def get(self, index: str, default: Optional[DataSet] = None) -> Optional[DataSet]: - """Get a block by its index or name. + """Get a block by its name. If the name is non-unique then returns the first occurrence. Returns ``default`` if name isn't in the dataset. From b853a4c87be5eb141e0c8d8af6b6a4e8b469514e Mon Sep 17 00:00:00 2001 From: Matthew Flamm Date: Sun, 24 Jul 2022 07:59:00 -0400 Subject: [PATCH 07/27] remove tuple assignment --- pyvista/core/composite.py | 13 +------------ tests/test_composite.py | 26 ++++++++++++++------------ 2 files changed, 15 insertions(+), 24 deletions(-) diff --git a/pyvista/core/composite.py b/pyvista/core/composite.py index 01f6f76a9c..3337fa04a8 100644 --- a/pyvista/core/composite.py +++ b/pyvista/core/composite.py @@ -456,7 +456,7 @@ def _ipython_key_completions_(self) -> List[Optional[str]]: def __setitem__( self, index: Union[int, str, slice], - data: Union[DataSet, Sequence[DataSet], Tuple[str, DataSet]], + data: Union[DataSet, Sequence[DataSet]], ): """Set a block with a VTK data object. @@ -477,24 +477,18 @@ def __setitem__( 3 """ - if isinstance(index, str) and isinstance(data, tuple): - raise TypeError(f"Cannot set key {index} with a different string key from {data}") - i: int = 0 name: Optional[str] = None if isinstance(index, str): try: i = self.get_index_by_name(index) except KeyError: - # data cannot be a tuple here if isinstance(data, collections.abc.Sequence): raise TypeError(f"Cannot set key {index} with the sequence {data}") self.append(data, index) return name = index elif isinstance(index, slice): - if isinstance(data, tuple): - raise TypeError(f"Cannot set the slice {slice} with a tuple {tuple}") for i, d in zip(range(self.n_blocks)[index], data): self[i] = d return @@ -502,11 +496,6 @@ def __setitem__( i = index # data, i, and name are a single value now - if isinstance(data, tuple): - if not len(data) == 2: - raise ValueError(f"Data {data} must be a length 2 tuple of form (DataSet, str)") - name, data = data - if data is not None and not is_pyvista_dataset(data): data = wrap(data) data = cast(pyvista.DataSet, data) diff --git a/tests/test_composite.py b/tests/test_composite.py index 9863e62e92..fbe758e541 100644 --- a/tests/test_composite.py +++ b/tests/test_composite.py @@ -123,7 +123,8 @@ def test_multi_block_set_get_ers(): assert multi.n_blocks == 6 # Check pyvista side registered it # Add data to the MultiBlock data = ex.load_rectilinear() - multi[1] = ('rect', data) + multi[1] = data + multi.set_block_name(1, 'rect') # Make sure number of blocks is constant assert multi.n_blocks == 6 # Check content @@ -179,11 +180,6 @@ def test_multi_block_set_get_ers(): with pytest.raises(TypeError): multi[1, 'foo'] = data - with pytest.raises(ValueError): - multi[1] = (UniformGrid(),) - - with pytest.raises(TypeError): - multi["rect"] = ("another key", UniformGrid()) with pytest.raises(TypeError): multi["not a key"] = [UniformGrid(), PolyData()] @@ -193,10 +189,14 @@ def test_multi_block_clean(rectilinear, uniform, ant): # now test a clean of the null values multi = MultiBlock() multi.n_blocks = 6 - multi[1] = ('rect', rectilinear) - multi[2] = ('empty', PolyData()) - multi[3] = ('mempty', MultiBlock()) - multi[5] = ('uni', uniform) + multi[1] = rectilinear + multi.set_block_name(1, 'rect') + multi[2] = PolyData() + multi.set_block_name(2, 'empty') + multi[3] = MultiBlock() + multi.set_block_name(3, 'mempty') + multi[5] = uniform + multi.set_block_name(5, 'uni') # perform the clean to remove all Null elements multi.clean() assert multi.n_blocks == 2 @@ -212,8 +212,10 @@ def test_multi_block_clean(rectilinear, uniform, ant): assert foo.n_blocks == 4 multi = MultiBlock() multi.n_blocks = 6 - multi[1] = ('rect', rectilinear) - multi[5] = ('multi', foo) + multi[1] = rectilinear + multi.set_block_name(1, 'rect') + multi[5] = foo + multi.set_block_name(5, 'multi') # perform the clean to remove all Null elements assert multi.n_blocks == 6 multi.clean() From 022907310ebb10c00c054b7fbad73bc0c47a4d2b Mon Sep 17 00:00:00 2001 From: Matthew Flamm Date: Sun, 24 Jul 2022 08:18:46 -0400 Subject: [PATCH 08/27] remove tuple assignment in docstring --- pyvista/core/composite.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/pyvista/core/composite.py b/pyvista/core/composite.py index 3337fa04a8..d8a6048c73 100644 --- a/pyvista/core/composite.py +++ b/pyvista/core/composite.py @@ -469,9 +469,8 @@ def __setitem__( >>> multi.append(pyvista.PolyData()) >>> multi[0] = pyvista.UnstructuredGrid() >>> multi.append(pyvista.PolyData(), 'poly') - >>> multi[1] = ('foo', pyvista.UnstructuredGrid()) >>> multi.keys() - ['Block-00', 'foo'] + ['Block-00', 'poly'] >>> multi['bar'] = pyvista.PolyData() >>> multi.n_blocks 3 From ce7ea17469fc6b0999427cb29a48d4d5af354ad2 Mon Sep 17 00:00:00 2001 From: Matthew Flamm Date: Sun, 24 Jul 2022 09:49:26 -0400 Subject: [PATCH 09/27] update docs and add versionchanged --- doc/api/core/composite.rst | 25 +++++++++++++++++-------- pyvista/core/composite.py | 4 ++++ 2 files changed, 21 insertions(+), 8 deletions(-) diff --git a/doc/api/core/composite.rst b/doc/api/core/composite.rst index e844718cd5..e0b237c817 100644 --- a/doc/api/core/composite.rst +++ b/doc/api/core/composite.rst @@ -4,29 +4,38 @@ Composite Datasets The :class:`pyvista.MultiBlock` class is a composite class to hold many data sets which can be iterated over. -You can think of MultiBlock like lists or dictionaries as we can -iterate over this data structure by index and we can also access -blocks by their string name. +MultiBlock behaves like a list, but also allows some dictionary +like features. We can iterate over this data structure by index, and we +can also access blocks by their string name. .. pyvista-plot:: Create empty composite dataset - >>> import pyvista - >>> blocks = pyvista.MultiBlock() + >>> import pyvista as pv + >>> blocks = pv.MultiBlock() Add a dataset to the collection - >>> blocks.append(pyvista.Sphere()) + >>> blocks.append(pv.Sphere()) - Or add a named block + Add a named block and access it by name like a dict - >>> blocks["cube"] = pyvista.Cube(center=(0, 0, -1)) + >>> blocks.append(pv.Cube(center=(0, 0, -1), "cube") + >>> blocks["cube"].bounds # same as blocks[1].bounds Plotting the MultiBlock plots all the meshes contained by it. >>> blocks.plot(smooth_shading=True) + It is also possible to append to the MultiBlock using a + nonexistent key + + >>> blocks["cone"] = pv.Cone() + + Duplicate and ``None`` keys are possible in MultiBlock, so the use of + dictionary features must be used with care. + Examples using this class: * :ref:`slice_example` diff --git a/pyvista/core/composite.py b/pyvista/core/composite.py index d8a6048c73..dc5ed0b6ec 100644 --- a/pyvista/core/composite.py +++ b/pyvista/core/composite.py @@ -32,6 +32,10 @@ class MultiBlock(_vtk.vtkMultiBlockDataSet, CompositeFilters, DataObject): can iterate over this data structure by index and we can also access blocks by their string name. + .. versionchanged:: 0.36.0 + ``MultiBlock`` adheres more closely to being list like. + Multiple nonconforming behaviors were removed or modified. + Examples -------- >>> import pyvista as pv From 623cb9f32e338437535e7d5f7ba4cc86a18ed049 Mon Sep 17 00:00:00 2001 From: MatthewFlamm <39341281+MatthewFlamm@users.noreply.github.com> Date: Sun, 24 Jul 2022 14:14:40 -0400 Subject: [PATCH 10/27] fix doc parentheses bug --- doc/api/core/composite.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/api/core/composite.rst b/doc/api/core/composite.rst index e0b237c817..da7e7db4da 100644 --- a/doc/api/core/composite.rst +++ b/doc/api/core/composite.rst @@ -21,7 +21,7 @@ can also access blocks by their string name. Add a named block and access it by name like a dict - >>> blocks.append(pv.Cube(center=(0, 0, -1), "cube") + >>> blocks.append(pv.Cube(center=(0, 0, -1)), "cube") >>> blocks["cube"].bounds # same as blocks[1].bounds Plotting the MultiBlock plots all the meshes contained by it. From df1b6169c289a1a86c56e6cdc92a282862dfe248 Mon Sep 17 00:00:00 2001 From: MatthewFlamm Date: Mon, 25 Jul 2022 02:10:50 +0000 Subject: [PATCH 11/27] more doc explanation --- doc/api/core/composite.rst | 34 ++++++++++++++++++++++++---------- 1 file changed, 24 insertions(+), 10 deletions(-) diff --git a/doc/api/core/composite.rst b/doc/api/core/composite.rst index da7e7db4da..5c001dfd91 100644 --- a/doc/api/core/composite.rst +++ b/doc/api/core/composite.rst @@ -4,7 +4,7 @@ Composite Datasets The :class:`pyvista.MultiBlock` class is a composite class to hold many data sets which can be iterated over. -MultiBlock behaves like a list, but also allows some dictionary +``MultiBlock`` behaves like a list, but also allows some dictionary like features. We can iterate over this data structure by index, and we can also access blocks by their string name. @@ -15,26 +15,40 @@ can also access blocks by their string name. >>> import pyvista as pv >>> blocks = pv.MultiBlock() - Add a dataset to the collection + Add some data to the collection. >>> blocks.append(pv.Sphere()) + >>> blocks.append(pv.Cube(center=(0, 0, -1))) - Add a named block and access it by name like a dict + Plotting the ``MultiBlock`` plots all the meshes contained by it. - >>> blocks.append(pv.Cube(center=(0, 0, -1)), "cube") - >>> blocks["cube"].bounds # same as blocks[1].bounds + >>> blocks.plot(smooth_shading=True) - Plotting the MultiBlock plots all the meshes contained by it. + ``MultiBlock`` is list-like, so individual blocks can be accessed via + indices. - >>> blocks.plot(smooth_shading=True) + >>> blocks[0] # Sphere + + ``MultiBlock`` also has some dictionary features. We can set the name + of the blocks, and then access them - It is also possible to append to the MultiBlock using a - nonexistent key + >>> blocks.set_block_name(0, "sphere") + >>> blocks.set_block_name(1, "cube") + >>> blocks["sphere"] # Sphere again + To append data, it is preferred to use :function:`pyvista.MultiBlock.append`. + A name can be set for the block. It is also possible to append to the MultiBlock using a + nonexistent key. + + >>> blocks.append(pv.Sphere(center=(-1, 0, 0)), "sphere2") >>> blocks["cone"] = pv.Cone() Duplicate and ``None`` keys are possible in MultiBlock, so the use of - dictionary features must be used with care. + dictionary-like features must be used with care. + + We can use slicing to retrieve or set multiple blocks. + + >>> blocks[0:1] Examples using this class: From f471028de671924599560a5003670b2d929e6f42 Mon Sep 17 00:00:00 2001 From: Matthew Flamm Date: Wed, 27 Jul 2022 11:49:44 -0400 Subject: [PATCH 12/27] Inherit from Sequence --- pyvista/core/composite.py | 62 +++++++++++++++++++++++++-------------- 1 file changed, 40 insertions(+), 22 deletions(-) diff --git a/pyvista/core/composite.py b/pyvista/core/composite.py index dc5ed0b6ec..b10cc385dc 100644 --- a/pyvista/core/composite.py +++ b/pyvista/core/composite.py @@ -6,7 +6,7 @@ import collections.abc import logging import pathlib -from typing import Any, List, Optional, Sequence, Tuple, Union, cast +from typing import Any, Iterable, List, Optional, Tuple, Union, cast, overload import numpy as np @@ -21,7 +21,10 @@ log.setLevel('CRITICAL') -class MultiBlock(_vtk.vtkMultiBlockDataSet, CompositeFilters, DataObject): +_TypeMultiBlockLeaf = Union['MultiBlock', DataSet] + + +class MultiBlock(_vtk.vtkMultiBlockDataSet, CompositeFilters, DataObject, collections.abc.Sequence): """A composite class to hold many data sets which can be iterated over. This wraps/extends the ``vtkMultiBlockDataSet`` class in VTK so @@ -285,7 +288,15 @@ def get_index_by_name(self, name: str) -> int: return i raise KeyError(f'Block name ({name}) not found') - def __getitem__(self, index: Union[int, str]) -> Optional['MultiBlock']: + @overload + def __getitem__(self, index: Union[int, str]) -> Optional[_TypeMultiBlockLeaf]: # noqa: D105 + ... + + @overload + def __getitem__(self, index: slice) -> 'MultiBlock': # noqa: D105 + ... + + def __getitem__(self, index): """Get a block by its index or name. If the name is non-unique then returns the first occurrence. @@ -311,12 +322,12 @@ def __getitem__(self, index: Union[int, str]) -> Optional['MultiBlock']: data = wrap(data) return data - def append(self, dataset: DataSet, name: Optional[str] = None): + def append(self, dataset: Optional[_TypeMultiBlockLeaf], name: Optional[str] = None): """Add a data set to the next block index. Parameters ---------- - dataset : pyvista.DataSet + dataset : pyvista.DataSet or pyvista.MultiBlock Dataset to append to this multi-block. name : str, optional @@ -346,7 +357,9 @@ def append(self, dataset: DataSet, name: Optional[str] = None): # No overwrite if name is None self.set_block_name(index, name) - def get(self, index: str, default: Optional[DataSet] = None) -> Optional[DataSet]: + def get( + self, index: str, default: Optional[_TypeMultiBlockLeaf] = None + ) -> Optional[_TypeMultiBlockLeaf]: """Get a block by its name. If the name is non-unique then returns the first occurrence. @@ -357,12 +370,12 @@ def get(self, index: str, default: Optional[DataSet] = None) -> Optional[DataSet index : str Index or name of the dataset within the multiblock. - default : pyvista.DataSet, optional + default : pyvista.DataSet or pyvista.MultiBlock, optional Default to return if index is not in the multiblock. Returns ------- - pyvista.DataSet or None + pyvista.DataSet or pyvista.MultiBlock or None Dataset from the given index if it exists. Examples @@ -457,10 +470,22 @@ def keys(self) -> List[Optional[str]]: def _ipython_key_completions_(self) -> List[Optional[str]]: return self.keys() + @overload + def __setitem__( + self, index: Union[int, str], data: Optional[_TypeMultiBlockLeaf] + ): # noqa: D105 + ... + + @overload + def __setitem__( + self, index: slice, data: Iterable[Optional[_TypeMultiBlockLeaf]] + ): # noqa: D105 + ... + def __setitem__( self, - index: Union[int, str, slice], - data: Union[DataSet, Sequence[DataSet]], + index, + data, ): """Set a block with a VTK data object. @@ -503,10 +528,7 @@ def __setitem__( data = wrap(data) data = cast(pyvista.DataSet, data) - if i < -self.n_blocks or i >= self.n_blocks: - raise IndexError(f'index ({i}) out of range for this dataset.') - if i < 0: - i = self.n_blocks + i + i = range(self.n_blocks)[i] # this is the only spot in the class where we actually add # data to the MultiBlock @@ -515,7 +537,6 @@ def __setitem__( existing_dataset = self.GetBlock(i) if existing_dataset is not None: self._remove_ref(i) - self.SetBlock(i, data) if data is not None: self._refs[data.memory_address] = data @@ -561,18 +582,15 @@ def __eq__(self, other): return True - def next(self) -> Optional['MultiBlock']: + def __next__(self) -> Optional[_TypeMultiBlockLeaf]: """Get the next block from the iterator.""" if self._iter_n < self.n_blocks: result = self[self._iter_n] self._iter_n += 1 return result - else: - raise StopIteration - - __next__ = next + raise StopIteration - def pop(self, index: Union[int, str]) -> Optional['MultiBlock']: + def pop(self, index: Union[int, str]) -> Optional[_TypeMultiBlockLeaf]: """Pop off a block at the specified index. Parameters @@ -582,7 +600,7 @@ def pop(self, index: Union[int, str]) -> Optional['MultiBlock']: Returns ------- - pyvista.DataSet + pyvista.DataSet or pyvista.MultiBlock Dataset from the given index. """ From dfc6297ebb31ee764537dc70d492e1a0d4951527 Mon Sep 17 00:00:00 2001 From: Matthew Flamm Date: Thu, 28 Jul 2022 09:09:56 -0400 Subject: [PATCH 13/27] Inherit from MutableSequence --- pyvista/core/composite.py | 48 +++++++++++++++++++++++++++++++++++---- tests/test_composite.py | 29 ++++++++++++++++++++++- 2 files changed, 72 insertions(+), 5 deletions(-) diff --git a/pyvista/core/composite.py b/pyvista/core/composite.py index b10cc385dc..df364e8ac9 100644 --- a/pyvista/core/composite.py +++ b/pyvista/core/composite.py @@ -24,7 +24,9 @@ _TypeMultiBlockLeaf = Union['MultiBlock', DataSet] -class MultiBlock(_vtk.vtkMultiBlockDataSet, CompositeFilters, DataObject, collections.abc.Sequence): +class MultiBlock( + _vtk.vtkMultiBlockDataSet, CompositeFilters, DataObject, collections.abc.MutableSequence +): """A composite class to hold many data sets which can be iterated over. This wraps/extends the ``vtkMultiBlockDataSet`` class in VTK so @@ -332,7 +334,7 @@ def append(self, dataset: Optional[_TypeMultiBlockLeaf], name: Optional[str] = N name : str, optional Block name to give to dataset. A default name is given - depending on the block index. + depending on the block index as 'Block-{i:02}'. Examples -------- @@ -416,6 +418,7 @@ def set_block_name(self, index: int, name: Optional[str]): ['cube', 'sphere', 'cone'] """ + index = range(self.n_blocks)[index] if name is None: return self.GetMetaData(index).Set(_vtk.vtkCompositeDataSet.NAME(), name) @@ -443,6 +446,7 @@ def get_block_name(self, index: int) -> Optional[str]: 'cube' """ + index = range(self.n_blocks)[index] meta = self.GetMetaData(index) if meta is not None: return meta.Get(_vtk.vtkCompositeDataSet.NAME()) @@ -545,8 +549,16 @@ def __setitem__( name = f'Block-{i:02}' self.set_block_name(i, name) # Note that this calls self.Modified() - def __delitem__(self, index: Union[int, str]): + def __delitem__(self, index: Union[int, str, slice]) -> None: """Remove a block at the specified index.""" + if isinstance(index, slice): + if index.indices(self.n_blocks)[2] > 0: + for i in reversed(range(*index.indices(self.n_blocks))): + self.__delitem__(i) + else: + for i in range(*index.indices(self.n_blocks)): + self.__delitem__(i) + return if isinstance(index, str): index = self.get_index_by_name(index) self._remove_ref(index) @@ -590,7 +602,31 @@ def __next__(self) -> Optional[_TypeMultiBlockLeaf]: return result raise StopIteration - def pop(self, index: Union[int, str]) -> Optional[_TypeMultiBlockLeaf]: + def insert(self, index: int, dataset: _TypeMultiBlockLeaf, name: Optional[str] = None) -> None: + """Insert data before index. + + Parameters + ---------- + index : int + Index before which to insert data. + dataset : pyvista.DataSet or pyvista.MultiBlock + Data to insert. + name : str, optional + Name for key to give dataset. A default name is given + depending on the block index as 'Block-{i:02}'. + + """ + index = range(self.n_blocks)[index] + + self.n_blocks += 1 + for i in reversed(range(index, self.n_blocks - 1)): + self[i + 1] = self[i] + self.set_block_name(i + 1, self.get_block_name(i)) + + self[index] = dataset + self.set_block_name(index, name) + + def pop(self, index: Optional[Union[int, str]] = None) -> Optional[_TypeMultiBlockLeaf]: """Pop off a block at the specified index. Parameters @@ -604,6 +640,10 @@ def pop(self, index: Union[int, str]) -> Optional[_TypeMultiBlockLeaf]: Dataset from the given index. """ + if isinstance(index, int): + index = range(self.n_blocks)[index] + if index is None: + index = self.n_blocks data = self[index] del self[index] return data diff --git a/tests/test_composite.py b/tests/test_composite.py index fbe758e541..b70e5a6089 100644 --- a/tests/test_composite.py +++ b/tests/test_composite.py @@ -157,7 +157,7 @@ def test_multi_block_set_get_ers(): pop = multi.pop(0) assert isinstance(pop, RectilinearGrid) assert multi.n_blocks == 3 - assert multi.get_block_name(10) is None + assert all([k is None for k in multi.keys()]) multi["new key"] = pyvista.Sphere() assert multi.n_blocks == 4 @@ -185,6 +185,33 @@ def test_multi_block_set_get_ers(): multi["not a key"] = [UniformGrid(), PolyData()] +def test_del_slice(): + multi = MultiBlock({f"{i}": pyvista.Sphere() for i in range(10)}) + del multi[0:10:2] + assert len(multi) == 5 + assert all([f"{i}" in multi.keys() for i in range(1, 10, 2)]) + + multi = MultiBlock({f"{i}": pyvista.Sphere() for i in range(10)}) + del multi[5:2:-1] + assert len(multi) == 7 + assert all([f"{i}" in multi.keys() for i in [0, 1, 2, 6, 7, 8, 9]]) + + +def test_insert(): + multi = MultiBlock({f"{i}": pyvista.Sphere() for i in range(3)}) + cube = pyvista.Cube() + multi.insert(0, cube) + assert len(multi) == 4 + assert multi[0] is cube + + # test with negative index and name + multi.insert(-1, pyvista.UniformGrid(), name="uni") + assert len(multi) == 5 + # inserted before last element + assert isinstance(multi[-2], pyvista.UniformGrid) # inserted before last element + assert multi.get_block_name(-2) == "uni" + + def test_multi_block_clean(rectilinear, uniform, ant): # now test a clean of the null values multi = MultiBlock() From dab9fbc9cf57345d34edbe9a7c60811f419021f7 Mon Sep 17 00:00:00 2001 From: Matthew Flamm Date: Fri, 29 Jul 2022 14:32:39 -0400 Subject: [PATCH 14/27] fix slicing in setitem --- pyvista/core/composite.py | 15 +++++++++++---- tests/test_composite.py | 35 +++++++++++++++++++++++++++++------ 2 files changed, 40 insertions(+), 10 deletions(-) diff --git a/pyvista/core/composite.py b/pyvista/core/composite.py index df364e8ac9..3bc351f881 100644 --- a/pyvista/core/composite.py +++ b/pyvista/core/composite.py @@ -4,6 +4,7 @@ to VTK algorithms and PyVista filtering/plotting routines. """ import collections.abc +from itertools import zip_longest import logging import pathlib from typing import Any, Iterable, List, Optional, Tuple, Union, cast, overload @@ -515,14 +516,20 @@ def __setitem__( try: i = self.get_index_by_name(index) except KeyError: - if isinstance(data, collections.abc.Sequence): - raise TypeError(f"Cannot set key {index} with the sequence {data}") self.append(data, index) return name = index elif isinstance(index, slice): - for i, d in zip(range(self.n_blocks)[index], data): - self[i] = d + index_iter = range(self.n_blocks)[index] + for i, (idx, d) in enumerate(zip_longest(index_iter, data)): + if idx is None: + self.insert( + index_iter[-1] + 1 + (i - len(index_iter)), d + ) # insert after last entry, increasing + elif d is None: + del self[index_iter[-1] + 1] # delete next entry + else: + self[idx] = d # return else: i = index diff --git a/tests/test_composite.py b/tests/test_composite.py index b70e5a6089..82cc74ae90 100644 --- a/tests/test_composite.py +++ b/tests/test_composite.py @@ -181,22 +181,45 @@ def test_multi_block_set_get_ers(): with pytest.raises(TypeError): multi[1, 'foo'] = data - with pytest.raises(TypeError): - multi["not a key"] = [UniformGrid(), PolyData()] - -def test_del_slice(): - multi = MultiBlock({f"{i}": pyvista.Sphere() for i in range(10)}) +def test_del_slice(sphere): + multi = MultiBlock({f"{i}": sphere for i in range(10)}) del multi[0:10:2] assert len(multi) == 5 assert all([f"{i}" in multi.keys() for i in range(1, 10, 2)]) - multi = MultiBlock({f"{i}": pyvista.Sphere() for i in range(10)}) + multi = MultiBlock({f"{i}": sphere for i in range(10)}) del multi[5:2:-1] assert len(multi) == 7 assert all([f"{i}" in multi.keys() for i in [0, 1, 2, 6, 7, 8, 9]]) +def test_slicing_multiple_in_setitem(sphere): + # equal length + multi = MultiBlock({f"{i}": sphere for i in range(10)}) + multi[1:3] = [pyvista.Cube(), pyvista.Cube()] + assert multi[1] == pyvista.Cube() + assert multi[2] == pyvista.Cube() + assert multi.count(pyvista.Cube()) == 2 + assert len(multi) == 10 + + # len(slice) < len(data) + multi = MultiBlock({f"{i}": sphere for i in range(10)}) + multi[1:3] = [pyvista.Cube(), pyvista.Cube(), pyvista.Cube()] + assert multi[1] == pyvista.Cube() + assert multi[2] == pyvista.Cube() + assert multi[3] == pyvista.Cube() + assert multi.count(pyvista.Cube()) == 3 + assert len(multi) == 11 + + # len(slice) > len(data) + multi = MultiBlock({f"{i}": sphere for i in range(10)}) + multi[1:3] = [pyvista.Cube()] + assert multi[1] == pyvista.Cube() + assert multi.count(pyvista.Cube()) == 1 + assert len(multi) == 9 + + def test_insert(): multi = MultiBlock({f"{i}": pyvista.Sphere() for i in range(3)}) cube = pyvista.Cube() From add4c359c12a7994f7c5c3a21d59dcc2ebf8d199 Mon Sep 17 00:00:00 2001 From: Matthew Flamm Date: Fri, 29 Jul 2022 15:21:26 -0400 Subject: [PATCH 15/27] Add reverse to preserve keys order --- pyvista/core/composite.py | 10 ++++++++++ tests/test_composite.py | 12 ++++++++++-- 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/pyvista/core/composite.py b/pyvista/core/composite.py index 3bc351f881..a63b129a1d 100644 --- a/pyvista/core/composite.py +++ b/pyvista/core/composite.py @@ -655,6 +655,16 @@ def pop(self, index: Optional[Union[int, str]] = None) -> Optional[_TypeMultiBlo del self[index] return data + def reverse(self): + """Reverse MultiBlock in-place.""" + # Taken from implementation in collections.abc.MutableSequence + names = self.keys() + n = len(self) + for i in range(n // 2): + self[i], self[n - i - 1] = self[n - i - 1], self[i] + for i, name in enumerate(reversed(names)): + self.set_block_name(i, name) + def clean(self, empty=True): """Remove any null blocks in place. diff --git a/tests/test_composite.py b/tests/test_composite.py index 82cc74ae90..c8161de6d5 100644 --- a/tests/test_composite.py +++ b/tests/test_composite.py @@ -220,8 +220,16 @@ def test_slicing_multiple_in_setitem(sphere): assert len(multi) == 9 -def test_insert(): - multi = MultiBlock({f"{i}": pyvista.Sphere() for i in range(3)}) +def test_reverse(sphere): + multi = MultiBlock({f"{i}": sphere for i in range(3)}) + multi.append(pyvista.Cube(), "cube") + multi.reverse() + assert multi[0] == pyvista.Cube() + assert np.array_equal(multi.keys(), ["cube", "2", "1", "0"]) + + +def test_insert(sphere): + multi = MultiBlock({f"{i}": sphere for i in range(3)}) cube = pyvista.Cube() multi.insert(0, cube) assert len(multi) == 4 From 658af8d00c289f512c36a41b08a98d1074cc5727 Mon Sep 17 00:00:00 2001 From: Matthew Flamm Date: Fri, 29 Jul 2022 16:34:57 -0400 Subject: [PATCH 16/27] Documentation updates --- doc/api/core/composite.rst | 73 ++++++++++++++++++++++++++++++-------- pyvista/core/composite.py | 11 +++--- 2 files changed, 64 insertions(+), 20 deletions(-) diff --git a/doc/api/core/composite.rst b/doc/api/core/composite.rst index 5c001dfd91..9f7e905777 100644 --- a/doc/api/core/composite.rst +++ b/doc/api/core/composite.rst @@ -2,17 +2,18 @@ Composite Datasets ================== The :class:`pyvista.MultiBlock` class is a composite class to hold many -data sets which can be iterated over. +data sets which can be iterated over. ``MultiBlock`` behaves mostly like +a list, but has some dictionary-like features. -``MultiBlock`` behaves like a list, but also allows some dictionary -like features. We can iterate over this data structure by index, and we -can also access blocks by their string name. +List-like Features +------------------ .. pyvista-plot:: Create empty composite dataset >>> import pyvista as pv + >>> from pyvista import examples >>> blocks = pv.MultiBlock() Add some data to the collection. @@ -29,26 +30,68 @@ can also access blocks by their string name. >>> blocks[0] # Sphere + The length of the block can be accessed through :func:`len` + + >>> len(blocks) + + or through the ``n_blocks`` attribute + + >>> blocks.n_blocks + + More specifically, ``MultiBlock`` is a :class:`collections.abc.MutableSequence` + and supports operations such as append, pop, insert, etc. Some of these operations + allow optional names to be provided for the dictionary like usage. TODO: link. + + >>> blocks.append(pv.Cone(), name="cone") + >>> cone = blocks.pop(-1) # Pops Cone + >>> blocks.reverse() + + ``MultiBlock`` also supports slicing for getting or setting blocks. + + >>> blocks[0:2] # The Sphere and Cube objects in a new ``MultiBlock`` + + +Dictionary-like Features +------------------------ +.. pyvista-plot:: + ``MultiBlock`` also has some dictionary features. We can set the name of the blocks, and then access them - + >>> blocks = pv.MultiBlock([pv.Sphere(), pv.Cube()]) >>> blocks.set_block_name(0, "sphere") >>> blocks.set_block_name(1, "cube") - >>> blocks["sphere"] # Sphere again + >>> blocks["sphere"] # Sphere + + It is important to note that ``MultiBlock`` is not a dictionary and does + not enforce unique keys. Keys can also be ``None``. Extra care must be + taken to avoid problems using the dictionary-like features. + + PyVista tries to keep the keys ordered correctly when doing list operations. + + >>> block.reverse() + >>> block.keys() + + The dictionary like features are useful when reading in data from a file. The + keys are often more understandable to access the data than the index. + :func:`pyvista.examples.download_cavity` is an OpenFoam dataset with a nested + ``MultiBlock`` structure. There are two entries in the top-level object + + >>> data = examples.download_cavity() + >>> data.keys() + + ``"internalMesh"`` is a :class:`pyvista.UnstructuredGrid`. - To append data, it is preferred to use :function:`pyvista.MultiBlock.append`. - A name can be set for the block. It is also possible to append to the MultiBlock using a - nonexistent key. + >>> data["internalMesh"] - >>> blocks.append(pv.Sphere(center=(-1, 0, 0)), "sphere2") - >>> blocks["cone"] = pv.Cone() + ``"boundary"`` is another :class:`pyvista.MultiBlock`. - Duplicate and ``None`` keys are possible in MultiBlock, so the use of - dictionary-like features must be used with care. + >>> data["boundary"] - We can use slicing to retrieve or set multiple blocks. + Using the dictionary like features of :class:`pyvista.MultiBlock` allow for easier + inspection and use of the data coming from an outside source. The names of each key + correspond to human understable portions of the dataset. - >>> blocks[0:1] + >>> data["boundary"].keys() Examples using this class: diff --git a/pyvista/core/composite.py b/pyvista/core/composite.py index a63b129a1d..627ec8ae2d 100644 --- a/pyvista/core/composite.py +++ b/pyvista/core/composite.py @@ -34,13 +34,14 @@ class MultiBlock( that we can easily plot these data sets and use the composite in a Pythonic manner. - You can think of ``MultiBlock`` like lists or dictionaries as we - can iterate over this data structure by index and we can also - access blocks by their string name. + You can think of ``MultiBlock`` like a list as we + can iterate over this data structure by index. It has some dictionary + features as we can also access blocks by their string name. .. versionchanged:: 0.36.0 - ``MultiBlock`` adheres more closely to being list like. - Multiple nonconforming behaviors were removed or modified. + ``MultiBlock`` adheres more closely to being list like, and inherits + from ``collections.abc.MutableSequence``. Multiple nonconforming + behaviors were removed or modified. Examples -------- From bf538a3c38d314183ab233d49d4500e1974c6abc Mon Sep 17 00:00:00 2001 From: MatthewFlamm Date: Sat, 30 Jul 2022 11:31:32 +0000 Subject: [PATCH 17/27] remove next from doc exclusions --- doc/conf.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/doc/conf.py b/doc/conf.py index 8ebdd60888..3ee8833fc6 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -201,8 +201,6 @@ # wraps r'\.Plotter\.enable_depth_peeling$', r'\.add_scalar_bar$', - # pending refactor - r'\.MultiBlock\.next$', # called from inherited r'\.Table\.copy_meta_from$', # Type alias From d404f1549fe0e8c2f4cb76ad4e40c6f15c8c86cf Mon Sep 17 00:00:00 2001 From: MatthewFlamm Date: Sat, 30 Jul 2022 12:57:10 +0000 Subject: [PATCH 18/27] use jupyter execute to capture outputs --- doc/api/core/composite.rst | 144 +++++++++++++++++++++++-------------- 1 file changed, 92 insertions(+), 52 deletions(-) diff --git a/doc/api/core/composite.rst b/doc/api/core/composite.rst index 9f7e905777..b75b61f858 100644 --- a/doc/api/core/composite.rst +++ b/doc/api/core/composite.rst @@ -8,90 +8,130 @@ a list, but has some dictionary-like features. List-like Features ------------------ -.. pyvista-plot:: +Create empty composite dataset - Create empty composite dataset +.. jupyter-execute:: + :hide-code: - >>> import pyvista as pv - >>> from pyvista import examples - >>> blocks = pv.MultiBlock() + # must have this here as our global backend may not be static + import pyvista + pyvista.set_plot_theme('document') + pyvista.set_jupyter_backend('pythreejs') + pyvista.global_theme.window_size = [600, 400] + pyvista.global_theme.axes.show = False + pyvista.global_theme.antialiasing = True + pyvista.global_theme.show_scalar_bar = False - Add some data to the collection. +.. jupyter-execute:: - >>> blocks.append(pv.Sphere()) - >>> blocks.append(pv.Cube(center=(0, 0, -1))) + import pyvista as pv + from pyvista import examples + blocks = pv.MultiBlock() + blocks - Plotting the ``MultiBlock`` plots all the meshes contained by it. +Add some data to the collection. - >>> blocks.plot(smooth_shading=True) +.. jupyter-execute:: - ``MultiBlock`` is list-like, so individual blocks can be accessed via - indices. + blocks.append(pv.Sphere()) + blocks.append(pv.Cube(center=(0, 0, -1))) - >>> blocks[0] # Sphere +Plotting the ``MultiBlock`` plots all the meshes contained by it. - The length of the block can be accessed through :func:`len` +.. jupyter-execute:: - >>> len(blocks) + blocks.plot(smooth_shading=True) - or through the ``n_blocks`` attribute +``MultiBlock`` is list-like, so individual blocks can be accessed via +indices. - >>> blocks.n_blocks +.. jupyter-execute:: - More specifically, ``MultiBlock`` is a :class:`collections.abc.MutableSequence` - and supports operations such as append, pop, insert, etc. Some of these operations - allow optional names to be provided for the dictionary like usage. TODO: link. + blocks[0] # Sphere - >>> blocks.append(pv.Cone(), name="cone") - >>> cone = blocks.pop(-1) # Pops Cone - >>> blocks.reverse() +The length of the block can be accessed through :func:`len` - ``MultiBlock`` also supports slicing for getting or setting blocks. +.. jupyter-execute:: - >>> blocks[0:2] # The Sphere and Cube objects in a new ``MultiBlock`` + len(blocks) + +or through the ``n_blocks`` attribute + +.. jupyter-execute:: + + blocks.n_blocks + +More specifically, ``MultiBlock`` is a :class:`collections.abc.MutableSequence` +and supports operations such as append, pop, insert, etc. Some of these operations +allow optional names to be provided for the dictionary like usage. + +.. jupyter-execute:: + + blocks.append(pv.Cone(), name="cone") + cone = blocks.pop(-1) # Pops Cone + blocks.reverse() + +``MultiBlock`` also supports slicing for getting or setting blocks. + +.. jupyter-execute:: + + blocks[0:2] # The Sphere and Cube objects in a new ``MultiBlock`` Dictionary-like Features ------------------------ -.. pyvista-plot:: - ``MultiBlock`` also has some dictionary features. We can set the name - of the blocks, and then access them - >>> blocks = pv.MultiBlock([pv.Sphere(), pv.Cube()]) - >>> blocks.set_block_name(0, "sphere") - >>> blocks.set_block_name(1, "cube") - >>> blocks["sphere"] # Sphere - It is important to note that ``MultiBlock`` is not a dictionary and does - not enforce unique keys. Keys can also be ``None``. Extra care must be - taken to avoid problems using the dictionary-like features. +``MultiBlock`` also has some dictionary features. We can set the name +of the blocks, and then access them + +.. jupyter-execute:: + + blocks = pv.MultiBlock([pv.Sphere(), pv.Cube()]) + blocks.set_block_name(0, "sphere") + blocks.set_block_name(1, "cube") + blocks["sphere"] # Sphere + +It is important to note that ``MultiBlock`` is not a dictionary and does +not enforce unique keys. Keys can also be ``None``. Extra care must be +taken to avoid problems using the dictionary-like features. + +PyVista tries to keep the keys ordered correctly when doing list operations. + +.. jupyter-execute:: + + blocks.reverse() + blocks.keys() + +The dictionary like features are useful when reading in data from a file. The +keys are often more understandable to access the data than the index. +:func:`pyvista.examples.download_cavity` is an OpenFoam dataset with a nested +``MultiBlock`` structure. There are two entries in the top-level object + +.. jupyter-execute:: - PyVista tries to keep the keys ordered correctly when doing list operations. + data = examples.download_cavity() + data.keys() - >>> block.reverse() - >>> block.keys() +``"internalMesh"`` is a :class:`pyvista.UnstructuredGrid`. - The dictionary like features are useful when reading in data from a file. The - keys are often more understandable to access the data than the index. - :func:`pyvista.examples.download_cavity` is an OpenFoam dataset with a nested - ``MultiBlock`` structure. There are two entries in the top-level object +.. jupyter-execute:: - >>> data = examples.download_cavity() - >>> data.keys() + data["internalMesh"] - ``"internalMesh"`` is a :class:`pyvista.UnstructuredGrid`. +``"boundary"`` is another :class:`pyvista.MultiBlock`. - >>> data["internalMesh"] +.. jupyter-execute:: - ``"boundary"`` is another :class:`pyvista.MultiBlock`. + data["boundary"] - >>> data["boundary"] +Using the dictionary like features of :class:`pyvista.MultiBlock` allow for easier +inspection and use of the data coming from an outside source. The names of each key +correspond to human understable portions of the dataset. - Using the dictionary like features of :class:`pyvista.MultiBlock` allow for easier - inspection and use of the data coming from an outside source. The names of each key - correspond to human understable portions of the dataset. +.. jupyter-execute:: - >>> data["boundary"].keys() + data["boundary"].keys() Examples using this class: From d6946070a2128703fa8df243c34f9f2da5f7b594 Mon Sep 17 00:00:00 2001 From: MatthewFlamm Date: Sat, 30 Jul 2022 13:01:31 +0000 Subject: [PATCH 19/27] dont cover typing overloads --- pyvista/core/composite.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/pyvista/core/composite.py b/pyvista/core/composite.py index 627ec8ae2d..7e42a9c554 100644 --- a/pyvista/core/composite.py +++ b/pyvista/core/composite.py @@ -294,11 +294,11 @@ def get_index_by_name(self, name: str) -> int: @overload def __getitem__(self, index: Union[int, str]) -> Optional[_TypeMultiBlockLeaf]: # noqa: D105 - ... + ... # pragma: no covrr @overload def __getitem__(self, index: slice) -> 'MultiBlock': # noqa: D105 - ... + ... # pragma: no cover def __getitem__(self, index): """Get a block by its index or name. @@ -480,13 +480,13 @@ def _ipython_key_completions_(self) -> List[Optional[str]]: def __setitem__( self, index: Union[int, str], data: Optional[_TypeMultiBlockLeaf] ): # noqa: D105 - ... + ... # pragma: no cover @overload def __setitem__( self, index: slice, data: Iterable[Optional[_TypeMultiBlockLeaf]] ): # noqa: D105 - ... + ... # pragma: no cover def __setitem__( self, From 257cec32625f654a38ef7feb82341f0d6c10e48d Mon Sep 17 00:00:00 2001 From: Matthew Flamm Date: Mon, 1 Aug 2022 13:13:05 -0400 Subject: [PATCH 20/27] remove mixing methods from numpydoc validation --- doc/conf.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/doc/conf.py b/doc/conf.py index 3ee8833fc6..11bea99a5c 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -205,6 +205,11 @@ r'\.Table\.copy_meta_from$', # Type alias r'\.color_like$', + # Mixin methods from collections.abc + r'\.MultiBlock\.clear$', + r'\.MultiBlock\.count$', + r'\.MultiBlock\.index$', + r'\.MultiBlock\.remove$', } From fcfef10e9f27bb7f42f789293de91c894fedcc7c Mon Sep 17 00:00:00 2001 From: Matthew Flamm Date: Mon, 1 Aug 2022 13:42:44 -0400 Subject: [PATCH 21/27] extend now preserves keys in a MultiBlock --- pyvista/core/composite.py | 31 +++++++++++++++++++++++++++++++ tests/test_composite.py | 18 ++++++++++++++++++ 2 files changed, 49 insertions(+) diff --git a/pyvista/core/composite.py b/pyvista/core/composite.py index 7e42a9c554..5e34872c60 100644 --- a/pyvista/core/composite.py +++ b/pyvista/core/composite.py @@ -361,6 +361,37 @@ def append(self, dataset: Optional[_TypeMultiBlockLeaf], name: Optional[str] = N # No overwrite if name is None self.set_block_name(index, name) + def extend(self, datasets: Iterable[_TypeMultiBlockLeaf]) -> None: + """Extend MultiBlock with an Iterable. + + If another MultiBlock object is supplied, the key names will + be preserved. + + Parameters + ---------- + datasets : Iterable[pyvista.DataSet or pyvista.MultiBlock] + Datasets to extend. + + Examples + -------- + >>> import pyvista as pv + >>> data = {"cube": pv.Cube(), "sphere": pv.Sphere(center=(2, 2, 0))} + >>> blocks = pv.MultiBlock(data) + >>> blocks_uniform = pv.MultiBlock({"uniform": examples.load_uniform()}) + >>> blocks.extend(blocks_uniform) + >>> len(blocks) + 3 + >>> blocks.keys() + ['cube', 'sphere', 'uniform'] + """ + # Code based on collections.abc + if isinstance(datasets, MultiBlock): + for key, data in zip(datasets.keys(), datasets): + self.append(data, key) + else: + for v in datasets: + self.append(v) + def get( self, index: str, default: Optional[_TypeMultiBlockLeaf] = None ) -> Optional[_TypeMultiBlockLeaf]: diff --git a/tests/test_composite.py b/tests/test_composite.py index c8161de6d5..3ce03b2292 100644 --- a/tests/test_composite.py +++ b/tests/test_composite.py @@ -243,6 +243,24 @@ def test_insert(sphere): assert multi.get_block_name(-2) == "uni" +def test_extend(sphere, uniform, ant): + # test with Iterable + multi = MultiBlock([sphere, ant]) + new_multi = [uniform, uniform] + multi.extend(new_multi) + assert len(multi) == 4 + assert multi.count(uniform) == 2 + + # test with a MultiBlock + multi = MultiBlock([sphere, ant]) + new_multi = MultiBlock({"uniform1": uniform, "uniform2": uniform}) + multi.extend(new_multi) + assert len(multi) == 4 + assert multi.count(uniform) == 2 + assert multi.keys()[-2] == "uniform1" + assert multi.keys()[-1] == "uniform2" + + def test_multi_block_clean(rectilinear, uniform, ant): # now test a clean of the null values multi = MultiBlock() From a126c053a77a7783ff8b46954248fa4c10f3c1a2 Mon Sep 17 00:00:00 2001 From: Matthew Flamm Date: Mon, 1 Aug 2022 14:06:57 -0400 Subject: [PATCH 22/27] fix example in extend docstring --- pyvista/core/composite.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pyvista/core/composite.py b/pyvista/core/composite.py index 5e34872c60..8890bb7217 100644 --- a/pyvista/core/composite.py +++ b/pyvista/core/composite.py @@ -375,6 +375,7 @@ def extend(self, datasets: Iterable[_TypeMultiBlockLeaf]) -> None: Examples -------- >>> import pyvista as pv + >>> from pyvista import examples >>> data = {"cube": pv.Cube(), "sphere": pv.Sphere(center=(2, 2, 0))} >>> blocks = pv.MultiBlock(data) >>> blocks_uniform = pv.MultiBlock({"uniform": examples.load_uniform()}) @@ -383,6 +384,7 @@ def extend(self, datasets: Iterable[_TypeMultiBlockLeaf]) -> None: 3 >>> blocks.keys() ['cube', 'sphere', 'uniform'] + """ # Code based on collections.abc if isinstance(datasets, MultiBlock): From 97de0eb36aff6e252b54a5f2aa762d4fbf9ac90f Mon Sep 17 00:00:00 2001 From: Matthew Flamm Date: Mon, 1 Aug 2022 14:49:22 -0400 Subject: [PATCH 23/27] fix pop with default and increase coverage --- pyvista/core/composite.py | 10 ++++------ tests/test_composite.py | 9 +++++++++ 2 files changed, 13 insertions(+), 6 deletions(-) diff --git a/pyvista/core/composite.py b/pyvista/core/composite.py index 8890bb7217..2c659dee77 100644 --- a/pyvista/core/composite.py +++ b/pyvista/core/composite.py @@ -667,24 +667,22 @@ def insert(self, index: int, dataset: _TypeMultiBlockLeaf, name: Optional[str] = self[index] = dataset self.set_block_name(index, name) - def pop(self, index: Optional[Union[int, str]] = None) -> Optional[_TypeMultiBlockLeaf]: + def pop(self, index: Union[int, str] = -1) -> Optional[_TypeMultiBlockLeaf]: """Pop off a block at the specified index. Parameters ---------- - index : int or str - Index or name of the dataset within the multiblock. + index : int or str, optional + Index or name of the dataset within the multiblock. Defaults to last dataset. Returns ------- pyvista.DataSet or pyvista.MultiBlock - Dataset from the given index. + Dataset from the given index that was removed. """ if isinstance(index, int): index = range(self.n_blocks)[index] - if index is None: - index = self.n_blocks data = self[index] del self[index] return data diff --git a/tests/test_composite.py b/tests/test_composite.py index 3ce03b2292..87a934d9ac 100644 --- a/tests/test_composite.py +++ b/tests/test_composite.py @@ -182,6 +182,15 @@ def test_multi_block_set_get_ers(): multi[1, 'foo'] = data +def test_pop(): + spheres = {f"{i}": pyvista.Sphere(phi_resolution=i + 3) for i in range(10)} + multi = MultiBlock(spheres) + assert multi.pop() == spheres["9"] + assert spheres["9"] not in multi + assert multi.pop(0) == spheres["0"] + assert spheres["0"] not in multi + + def test_del_slice(sphere): multi = MultiBlock({f"{i}": sphere for i in range(10)}) del multi[0:10:2] From ba452c2c4375b1520eb27aa317ef5dd4a6dc5155 Mon Sep 17 00:00:00 2001 From: Matthew Flamm Date: Mon, 1 Aug 2022 14:53:21 -0400 Subject: [PATCH 24/27] typo for skipping coverage in overload of __getitem__ --- pyvista/core/composite.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyvista/core/composite.py b/pyvista/core/composite.py index 2c659dee77..596251ed8d 100644 --- a/pyvista/core/composite.py +++ b/pyvista/core/composite.py @@ -294,7 +294,7 @@ def get_index_by_name(self, name: str) -> int: @overload def __getitem__(self, index: Union[int, str]) -> Optional[_TypeMultiBlockLeaf]: # noqa: D105 - ... # pragma: no covrr + ... # pragma: no cover @overload def __getitem__(self, index: slice) -> 'MultiBlock': # noqa: D105 From 20323ae4cd4bbbfb309aa5564c45917ebc0aba8d Mon Sep 17 00:00:00 2001 From: Matthew Flamm Date: Mon, 1 Aug 2022 15:53:55 -0400 Subject: [PATCH 25/27] add replace method --- pyvista/core/composite.py | 26 ++++++++++++++++++++++++++ tests/test_composite.py | 9 +++++++++ 2 files changed, 35 insertions(+) diff --git a/pyvista/core/composite.py b/pyvista/core/composite.py index 596251ed8d..3644f5e113 100644 --- a/pyvista/core/composite.py +++ b/pyvista/core/composite.py @@ -509,6 +509,32 @@ def keys(self) -> List[Optional[str]]: def _ipython_key_completions_(self) -> List[Optional[str]]: return self.keys() + def replace(self, index: int, dataset: Optional[_TypeMultiBlockLeaf]) -> None: + """Replace dataset at index while preserving key name. + + Parameters + ---------- + index : int + Index of the block to replace. + dataset : pyvista.DataSet or pyvista.MultiBlock + Dataset for replacing the one at index. + + Examples + -------- + >>> import pyvista as pv + >>> data = {"cube": pv.Cube(), "sphere": pv.Sphere(center=(2, 2, 0))} + >>> blocks = pv.MultiBlock(data) + >>> blocks.replace(1, pv.Sphere(center=(10, 10, 10))) + >>> blocks.keys() + ['cube', 'sphere'] + >>> np.allclose(blocks[1].center, [10., 10., 10.]) + True + + """ + name = self.get_block_name(index) + self[index] = dataset + self.set_block_name(index, name) + @overload def __setitem__( self, index: Union[int, str], data: Optional[_TypeMultiBlockLeaf] diff --git a/tests/test_composite.py b/tests/test_composite.py index 87a934d9ac..14262aa48b 100644 --- a/tests/test_composite.py +++ b/tests/test_composite.py @@ -182,6 +182,15 @@ def test_multi_block_set_get_ers(): multi[1, 'foo'] = data +def test_replace(): + spheres = {f"{i}": pyvista.Sphere(phi_resolution=i + 3) for i in range(10)} + multi = MultiBlock(spheres) + cube = pyvista.Cube() + multi.replace(3, cube) + assert multi.get_block_name(3) == "3" + assert multi[3] is cube + + def test_pop(): spheres = {f"{i}": pyvista.Sphere(phi_resolution=i + 3) for i in range(10)} multi = MultiBlock(spheres) From dbd65a58d8717f320c79ec0569184abd30e61433 Mon Sep 17 00:00:00 2001 From: Matthew Flamm Date: Mon, 1 Aug 2022 16:24:27 -0400 Subject: [PATCH 26/27] fix numpy import in docstring examples --- pyvista/core/composite.py | 1 + 1 file changed, 1 insertion(+) diff --git a/pyvista/core/composite.py b/pyvista/core/composite.py index 3644f5e113..70ea0fc963 100644 --- a/pyvista/core/composite.py +++ b/pyvista/core/composite.py @@ -522,6 +522,7 @@ def replace(self, index: int, dataset: Optional[_TypeMultiBlockLeaf]) -> None: Examples -------- >>> import pyvista as pv + >>> import numpy as np >>> data = {"cube": pv.Cube(), "sphere": pv.Sphere(center=(2, 2, 0))} >>> blocks = pv.MultiBlock(data) >>> blocks.replace(1, pv.Sphere(center=(10, 10, 10))) From 24d4941c9b72f60c4fdb7a8e16428e97337c9f64 Mon Sep 17 00:00:00 2001 From: Alex Kaszynski Date: Sat, 6 Aug 2022 19:30:31 +0000 Subject: [PATCH 27/27] misc doc edits --- pyvista/core/composite.py | 59 +++++++++++++++++++++++++++++++++------ 1 file changed, 51 insertions(+), 8 deletions(-) diff --git a/pyvista/core/composite.py b/pyvista/core/composite.py index 70ea0fc963..5b4f129f52 100644 --- a/pyvista/core/composite.py +++ b/pyvista/core/composite.py @@ -30,8 +30,9 @@ class MultiBlock( ): """A composite class to hold many data sets which can be iterated over. - This wraps/extends the ``vtkMultiBlockDataSet`` class in VTK so - that we can easily plot these data sets and use the composite in a + This wraps/extends the `vtkMultiBlockDataSet + `_ class + so that we can easily plot these data sets and use the composite in a Pythonic manner. You can think of ``MultiBlock`` like a list as we @@ -40,14 +41,14 @@ class MultiBlock( .. versionchanged:: 0.36.0 ``MultiBlock`` adheres more closely to being list like, and inherits - from ``collections.abc.MutableSequence``. Multiple nonconforming + from :class:`collections.abc.MutableSequence`. Multiple nonconforming behaviors were removed or modified. Examples -------- >>> import pyvista as pv - Create empty composite dataset + Create an empty composite dataset. >>> blocks = pv.MultiBlock() @@ -73,7 +74,7 @@ class MultiBlock( >>> blocks = pv.MultiBlock(data) >>> blocks.plot() - Iterate over the collection + Iterate over the collection. >>> for name in blocks.keys(): ... block = blocks[name] @@ -681,7 +682,20 @@ def insert(self, index: int, dataset: _TypeMultiBlockLeaf, name: Optional[str] = Data to insert. name : str, optional Name for key to give dataset. A default name is given - depending on the block index as 'Block-{i:02}'. + depending on the block index as ``'Block-{i:02}'``. + + Examples + -------- + Insert a new :class:`pyvista.PolyData` at the start of the multiblock. + + >>> import pyvista as pv + >>> data = {"cube": pv.Cube(), "sphere": pv.Sphere(center=(2, 2, 0))} + >>> blocks = pv.MultiBlock(data) + >>> blocks.keys() + ['cube', 'sphere'] + >>> blocks.insert(0, pv.Plane(), "plane") + >>> blocks.keys() + ['plane', 'cube', 'sphere'] """ index = range(self.n_blocks)[index] @@ -700,13 +714,27 @@ def pop(self, index: Union[int, str] = -1) -> Optional[_TypeMultiBlockLeaf]: Parameters ---------- index : int or str, optional - Index or name of the dataset within the multiblock. Defaults to last dataset. + Index or name of the dataset within the multiblock. Defaults to + last dataset. Returns ------- pyvista.DataSet or pyvista.MultiBlock Dataset from the given index that was removed. + Examples + -------- + Pop the ``"cube"`` multiblock. + + >>> import pyvista as pv + >>> data = {"cube": pv.Cube(), "sphere": pv.Sphere(center=(2, 2, 0))} + >>> blocks = pv.MultiBlock(data) + >>> blocks.keys() + ['cube', 'sphere'] + >>> cube = blocks.pop("cube") + >>> blocks.keys() + ['sphere'] + """ if isinstance(index, int): index = range(self.n_blocks)[index] @@ -715,7 +743,22 @@ def pop(self, index: Union[int, str] = -1) -> Optional[_TypeMultiBlockLeaf]: return data def reverse(self): - """Reverse MultiBlock in-place.""" + """Reverse MultiBlock in-place. + + Examples + -------- + Reverse a multiblock. + + >>> import pyvista as pv + >>> data = {"cube": pv.Cube(), "sphere": pv.Sphere(center=(2, 2, 0))} + >>> blocks = pv.MultiBlock(data) + >>> blocks.keys() + ['cube', 'sphere'] + >>> blocks.reverse() + >>> blocks.keys() + ['sphere', 'cube'] + + """ # Taken from implementation in collections.abc.MutableSequence names = self.keys() n = len(self)