sliceview 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Juliano Fischer Naves
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,153 @@
1
+ Metadata-Version: 2.4
2
+ Name: sliceview
3
+ Version: 0.1.0
4
+ Summary: Zero-copy, composable slice views for Python sequences
5
+ Project-URL: Homepage, https://github.com/julianofischer/sliceview
6
+ Project-URL: Bug Tracker, https://github.com/julianofischer/sliceview/issues
7
+ Project-URL: Discussions, https://discuss.python.org/t/slice-views-for-python-sequences/103531
8
+ Author-email: Juliano Fischer Naves <julianofischer@gmail.com>
9
+ License: MIT
10
+ License-File: LICENSE
11
+ Keywords: memory,performance,sequence,slice,view,zero-copy
12
+ Classifier: Development Status :: 3 - Alpha
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: License :: OSI Approved :: MIT License
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Programming Language :: Python :: 3.9
17
+ Classifier: Programming Language :: Python :: 3.10
18
+ Classifier: Programming Language :: Python :: 3.11
19
+ Classifier: Programming Language :: Python :: 3.12
20
+ Classifier: Programming Language :: Python :: 3.13
21
+ Classifier: Topic :: Software Development :: Libraries
22
+ Classifier: Topic :: Utilities
23
+ Requires-Python: >=3.9
24
+ Provides-Extra: dev
25
+ Requires-Dist: pytest-cov; extra == 'dev'
26
+ Requires-Dist: pytest>=7.0; extra == 'dev'
27
+ Description-Content-Type: text/markdown
28
+
29
+ # sliceview
30
+
31
+ **Zero-copy, composable slice views for Python sequences.**
32
+
33
+ `sliceview` lets you work with windows into lists, tuples, strings, or any
34
+ `Sequence` without copying the underlying data. It is the proof-of-concept
35
+ library accompanying the Python discussion on
36
+ [Slice Views for Python Sequences](https://discuss.python.org/t/slice-views-for-python-sequences/103531).
37
+
38
+ ```python
39
+ from sliceview import sliceview
40
+
41
+ data = list(range(1_000_000))
42
+
43
+ # O(1) — no copy
44
+ window = sliceview(data)[50_000:60_000]
45
+
46
+ # Compose slices — still O(1), still no copy
47
+ every_third = sliceview(data)[::3][100:200]
48
+
49
+ # In-place windowed update
50
+ sv = sliceview(data)
51
+ sv[0:5] = [10, 20, 30, 40, 50]
52
+
53
+ # Sliding window without creating new objects
54
+ view = sliceview(data, 0, 1000)
55
+ for _ in range(10):
56
+ process(view)
57
+ view.advance(1000)
58
+ ```
59
+
60
+ ## Why?
61
+
62
+ In Python today, `seq[a:b]` copies the data. For large pipelines — text
63
+ processing, audio, genomics, sorting algorithms — those copies dominate
64
+ both time and memory. `memoryview` solves this for *bytes-like* objects;
65
+ `sliceview` targets *generic* sequences of Python objects.
66
+
67
+ Inspired by Go slices, NumPy views, and `memoryview`.
68
+
69
+ ## Installation
70
+
71
+ ```bash
72
+ pip install sliceview
73
+ ```
74
+
75
+ ## Features
76
+
77
+ | Feature | Details |
78
+ |---|---|
79
+ | **Zero-copy slicing** | `sv[a:b:c]` returns a new `sliceview` in O(1) |
80
+ | **Composable** | `sv[2:][::3][5:10]` chains correctly with no intermediate copies |
81
+ | **Live view** | Mutations to the base are immediately visible through the view |
82
+ | **Write-through** | `sv[i] = x` and `sv[a:b] = iterable` forward to the base |
83
+ | **Sliding window** | `sv.advance(n)` shifts the window in-place — no new object |
84
+ | **Any sequence** | Works with `list`, `tuple`, `str`, `array`, or your own type |
85
+
86
+ ## API
87
+
88
+ ### `sliceview(base, start=None, stop=None, step=None)`
89
+
90
+ Create a view over `base`. `start` may be a `slice` object.
91
+
92
+ ```python
93
+ sv = sliceview(my_list) # full view
94
+ sv = sliceview(my_list, 10, 20) # [10:20]
95
+ sv = sliceview(my_list, slice(10, 20, 2)) # [10:20:2]
96
+ ```
97
+
98
+ ### Indexing and slicing
99
+
100
+ ```python
101
+ sv[i] # element access — maps to base[start + i*step]
102
+ sv[a:b:c] # returns a new sliceview (O(1))
103
+ sv[i] = x # write-through to the base
104
+ sv[a:b] = it # slice assignment (delegates to base)
105
+ ```
106
+
107
+ ### `sv.advance(n) -> self`
108
+
109
+ Shift the view's window forward by `n` index positions (negative to retreat).
110
+ Returns `self` for chaining. Useful for sliding-window algorithms:
111
+
112
+ ```python
113
+ view = sliceview(samples, 0, window_size)
114
+ while True:
115
+ result = process(view)
116
+ if view.advance(window_size)._start >= len(samples):
117
+ break
118
+ ```
119
+
120
+ ### `sv.tolist()` / `sv.copy()`
121
+
122
+ Materialise the view as a new `list` (explicit copy).
123
+
124
+ ### `sv.base`
125
+
126
+ The underlying sequence the view points into.
127
+
128
+ ## Semantics
129
+
130
+ - **`len(sv)`** reflects the *current* base length, so appending to the base
131
+ is immediately visible.
132
+ - **`sv[:]`** returns a new `sliceview` pointing at the same base — O(1).
133
+ - **Hashing**: `sliceview` is intentionally unhashable.
134
+ - **Equality**: compares element-wise to any `Sequence`.
135
+ - **Immutable bases**: `sv[i] = x` raises `TypeError` if the base does not
136
+ support `__setitem__`.
137
+
138
+ ## Design notes and open questions
139
+
140
+ This library implements the core proposal from the
141
+ [Python discussion](https://discuss.python.org/t/slice-views-for-python-sequences/103531).
142
+ Deliberately left out to keep the scope focused:
143
+
144
+ - `view()` builtin shortcut (consensus in the thread was it's unnecessary)
145
+ - `__sliceview__` dunder (motivation unclear until adoption data exists)
146
+ - Multidimensional views (NumPy is the right tool for that)
147
+ - ABC / Protocol additions to `collections.abc`
148
+
149
+ Feedback welcome — please open an issue or join the discussion thread.
150
+
151
+ ## License
152
+
153
+ MIT
@@ -0,0 +1,125 @@
1
+ # sliceview
2
+
3
+ **Zero-copy, composable slice views for Python sequences.**
4
+
5
+ `sliceview` lets you work with windows into lists, tuples, strings, or any
6
+ `Sequence` without copying the underlying data. It is the proof-of-concept
7
+ library accompanying the Python discussion on
8
+ [Slice Views for Python Sequences](https://discuss.python.org/t/slice-views-for-python-sequences/103531).
9
+
10
+ ```python
11
+ from sliceview import sliceview
12
+
13
+ data = list(range(1_000_000))
14
+
15
+ # O(1) — no copy
16
+ window = sliceview(data)[50_000:60_000]
17
+
18
+ # Compose slices — still O(1), still no copy
19
+ every_third = sliceview(data)[::3][100:200]
20
+
21
+ # In-place windowed update
22
+ sv = sliceview(data)
23
+ sv[0:5] = [10, 20, 30, 40, 50]
24
+
25
+ # Sliding window without creating new objects
26
+ view = sliceview(data, 0, 1000)
27
+ for _ in range(10):
28
+ process(view)
29
+ view.advance(1000)
30
+ ```
31
+
32
+ ## Why?
33
+
34
+ In Python today, `seq[a:b]` copies the data. For large pipelines — text
35
+ processing, audio, genomics, sorting algorithms — those copies dominate
36
+ both time and memory. `memoryview` solves this for *bytes-like* objects;
37
+ `sliceview` targets *generic* sequences of Python objects.
38
+
39
+ Inspired by Go slices, NumPy views, and `memoryview`.
40
+
41
+ ## Installation
42
+
43
+ ```bash
44
+ pip install sliceview
45
+ ```
46
+
47
+ ## Features
48
+
49
+ | Feature | Details |
50
+ |---|---|
51
+ | **Zero-copy slicing** | `sv[a:b:c]` returns a new `sliceview` in O(1) |
52
+ | **Composable** | `sv[2:][::3][5:10]` chains correctly with no intermediate copies |
53
+ | **Live view** | Mutations to the base are immediately visible through the view |
54
+ | **Write-through** | `sv[i] = x` and `sv[a:b] = iterable` forward to the base |
55
+ | **Sliding window** | `sv.advance(n)` shifts the window in-place — no new object |
56
+ | **Any sequence** | Works with `list`, `tuple`, `str`, `array`, or your own type |
57
+
58
+ ## API
59
+
60
+ ### `sliceview(base, start=None, stop=None, step=None)`
61
+
62
+ Create a view over `base`. `start` may be a `slice` object.
63
+
64
+ ```python
65
+ sv = sliceview(my_list) # full view
66
+ sv = sliceview(my_list, 10, 20) # [10:20]
67
+ sv = sliceview(my_list, slice(10, 20, 2)) # [10:20:2]
68
+ ```
69
+
70
+ ### Indexing and slicing
71
+
72
+ ```python
73
+ sv[i] # element access — maps to base[start + i*step]
74
+ sv[a:b:c] # returns a new sliceview (O(1))
75
+ sv[i] = x # write-through to the base
76
+ sv[a:b] = it # slice assignment (delegates to base)
77
+ ```
78
+
79
+ ### `sv.advance(n) -> self`
80
+
81
+ Shift the view's window forward by `n` index positions (negative to retreat).
82
+ Returns `self` for chaining. Useful for sliding-window algorithms:
83
+
84
+ ```python
85
+ view = sliceview(samples, 0, window_size)
86
+ while True:
87
+ result = process(view)
88
+ if view.advance(window_size)._start >= len(samples):
89
+ break
90
+ ```
91
+
92
+ ### `sv.tolist()` / `sv.copy()`
93
+
94
+ Materialise the view as a new `list` (explicit copy).
95
+
96
+ ### `sv.base`
97
+
98
+ The underlying sequence the view points into.
99
+
100
+ ## Semantics
101
+
102
+ - **`len(sv)`** reflects the *current* base length, so appending to the base
103
+ is immediately visible.
104
+ - **`sv[:]`** returns a new `sliceview` pointing at the same base — O(1).
105
+ - **Hashing**: `sliceview` is intentionally unhashable.
106
+ - **Equality**: compares element-wise to any `Sequence`.
107
+ - **Immutable bases**: `sv[i] = x` raises `TypeError` if the base does not
108
+ support `__setitem__`.
109
+
110
+ ## Design notes and open questions
111
+
112
+ This library implements the core proposal from the
113
+ [Python discussion](https://discuss.python.org/t/slice-views-for-python-sequences/103531).
114
+ Deliberately left out to keep the scope focused:
115
+
116
+ - `view()` builtin shortcut (consensus in the thread was it's unnecessary)
117
+ - `__sliceview__` dunder (motivation unclear until adoption data exists)
118
+ - Multidimensional views (NumPy is the right tool for that)
119
+ - ABC / Protocol additions to `collections.abc`
120
+
121
+ Feedback welcome — please open an issue or join the discussion thread.
122
+
123
+ ## License
124
+
125
+ MIT
@@ -0,0 +1,280 @@
1
+ """
2
+ sliceview — Zero-copy slice views for Python sequences.
3
+
4
+ A sliceview presents a live window into an existing sequence:
5
+ reads and writes reflect the underlying sequence, view-to-view
6
+ slicing composes in O(1), and no data is copied unless explicitly
7
+ requested.
8
+
9
+ Basic usage::
10
+
11
+ from sliceview import sliceview
12
+
13
+ big = list(range(1_000_000))
14
+
15
+ # O(1) window — no copy
16
+ window = sliceview(big)[1000:2000]
17
+
18
+ # Compose slices — still O(1)
19
+ every_other = sliceview(big)[::2][500:1000]
20
+
21
+ # In-place windowed update
22
+ sv = sliceview(big)
23
+ sv[0:10] = range(10, 20)
24
+
25
+ # Sliding window without creating new objects
26
+ view = sliceview(big, 0, 100)
27
+ for _ in range(10):
28
+ view.advance(100)
29
+ """
30
+
31
+ from __future__ import annotations
32
+
33
+ from collections.abc import Sequence, Iterator
34
+ from typing import overload, Union, Optional
35
+
36
+
37
+ __all__ = ["sliceview"]
38
+ __version__ = "0.1.0"
39
+
40
+
41
+ class _OpenRange:
42
+ """Sentinel for a range whose stop is open (None), so it grows with the base."""
43
+
44
+ __slots__ = ("start", "step")
45
+
46
+ def __init__(self, start: int, step: int) -> None:
47
+ self.start = start
48
+ self.step = step
49
+
50
+ def resolve(self, b_len: int) -> range:
51
+ stop = b_len if self.step > 0 else -1
52
+ return range(self.start, stop, self.step)
53
+
54
+
55
+ class sliceview(Sequence):
56
+ """A zero-copy, composable slice view over any :class:`collections.abc.Sequence`.
57
+
58
+ Parameters
59
+ ----------
60
+ base:
61
+ The underlying sequence. Any object that implements
62
+ ``__len__`` and ``__getitem__`` with integer indices is accepted.
63
+ start, stop, step:
64
+ Slice parameters (same semantics as the built-in :class:`slice`).
65
+ *start* may alternatively be a :class:`slice` object, in which case
66
+ *stop* and *step* must be omitted.
67
+
68
+ Examples
69
+ --------
70
+ >>> sv = sliceview([0, 1, 2, 3, 4, 5])
71
+ >>> list(sv[1:4])
72
+ [1, 2, 3]
73
+ >>> list(sv[::2])
74
+ [0, 2, 4]
75
+ >>> sv2 = sv[1:][::2] # composed — O(1), no copy
76
+ >>> list(sv2)
77
+ [1, 3, 5]
78
+ """
79
+
80
+ __slots__ = ("_base", "_range")
81
+
82
+ # ------------------------------------------------------------------
83
+ # Construction
84
+ # ------------------------------------------------------------------
85
+
86
+ def __init__(
87
+ self,
88
+ base: Sequence,
89
+ start: Union[int, slice, None] = None,
90
+ stop: Optional[int] = None,
91
+ step: Optional[int] = None,
92
+ ) -> None:
93
+ if not (hasattr(base, "__len__") and hasattr(base, "__getitem__")):
94
+ raise TypeError(
95
+ f"sliceview requires a sequence with __len__ and __getitem__, "
96
+ f"got {type(base).__name__!r}"
97
+ )
98
+ self._base = base
99
+
100
+ if isinstance(start, slice) and stop is None and step is None:
101
+ sl: slice = start
102
+ else:
103
+ sl = slice(start, stop, step)
104
+
105
+ b_len = len(base)
106
+ s, e, st = sl.indices(b_len)
107
+
108
+ # If the original stop was None, store an open sentinel so that the
109
+ # view grows when elements are appended to the base.
110
+ if sl.stop is None:
111
+ self._range = _OpenRange(s, st)
112
+ else:
113
+ self._range = range(s, e, st)
114
+
115
+ @classmethod
116
+ def _from_range(cls, base: Sequence, r) -> "sliceview":
117
+ """Internal constructor: build a view directly from a range object."""
118
+ sv = cls.__new__(cls)
119
+ sv._base = base
120
+ sv._range = r
121
+ return sv
122
+
123
+ # ------------------------------------------------------------------
124
+ # Core helper
125
+ # ------------------------------------------------------------------
126
+
127
+ def _current_range(self) -> range:
128
+ """Return a concrete ``range`` clamped to the current base length."""
129
+ r = self._range
130
+ if isinstance(r, _OpenRange):
131
+ return r.resolve(len(self._base))
132
+ return r
133
+
134
+ # ------------------------------------------------------------------
135
+ # Sequence protocol
136
+ # ------------------------------------------------------------------
137
+
138
+ def __len__(self) -> int:
139
+ return len(self._current_range())
140
+
141
+ @overload
142
+ def __getitem__(self, index: int) -> object: ...
143
+ @overload
144
+ def __getitem__(self, index: slice) -> "sliceview": ...
145
+
146
+ def __getitem__(self, index):
147
+ if isinstance(index, slice):
148
+ # Compose slices using Python's range slicing — O(1), exact.
149
+ sub = self._current_range()[index]
150
+ return sliceview._from_range(self._base, sub)
151
+
152
+ cr = self._current_range()
153
+ length = len(cr)
154
+ if index < 0:
155
+ index += length
156
+ if not (0 <= index < length):
157
+ raise IndexError("sliceview index out of range")
158
+ return self._base[cr[index]]
159
+
160
+ def __setitem__(self, index, value) -> None:
161
+ if not hasattr(self._base, "__setitem__"):
162
+ raise TypeError("underlying sequence is not mutable")
163
+
164
+ if isinstance(index, slice):
165
+ self._setslice(index, value)
166
+ return
167
+
168
+ cr = self._current_range()
169
+ length = len(cr)
170
+ if index < 0:
171
+ index += length
172
+ if not (0 <= index < length):
173
+ raise IndexError("sliceview index out of range")
174
+ self._base[cr[index]] = value
175
+
176
+ def __iter__(self) -> Iterator:
177
+ base = self._base
178
+ for i in self._current_range():
179
+ yield base[i]
180
+
181
+ def __contains__(self, item) -> bool:
182
+ return any(item == el for el in self)
183
+
184
+ def __reversed__(self) -> Iterator:
185
+ base = self._base
186
+ return (base[i] for i in reversed(self._current_range()))
187
+
188
+ # ------------------------------------------------------------------
189
+ # Equality and hashing
190
+ # ------------------------------------------------------------------
191
+
192
+ def __eq__(self, other) -> bool:
193
+ if isinstance(other, Sequence):
194
+ return len(self) == len(other) and all(a == b for a, b in zip(self, other))
195
+ return NotImplemented
196
+
197
+ __hash__ = None # type: ignore[assignment] # unhashable by design
198
+
199
+ # ------------------------------------------------------------------
200
+ # Repr
201
+ # ------------------------------------------------------------------
202
+
203
+ def __repr__(self) -> str:
204
+ cr = self._current_range()
205
+ return f"sliceview({self._base!r})[{cr.start}:{cr.stop}:{cr.step}]"
206
+
207
+ # ------------------------------------------------------------------
208
+ # Advance — sliding-window helper
209
+ # ------------------------------------------------------------------
210
+
211
+ def advance(self, n: int) -> "sliceview":
212
+ """Shift the view's window forward by *n* index positions in-place.
213
+
214
+ Returns *self* so calls can be chained. Useful for sliding windows:
215
+
216
+ >>> data = list(range(10))
217
+ >>> sv = sliceview(data, 0, 3)
218
+ >>> list(sv)
219
+ [0, 1, 2]
220
+ >>> sv.advance(3) # doctest: +ELLIPSIS
221
+ sliceview(...)[3:6:1]
222
+ >>> list(sv)
223
+ [3, 4, 5]
224
+
225
+ Parameters
226
+ ----------
227
+ n:
228
+ Positions to advance (negative to retreat).
229
+ """
230
+ b_len = len(self._base)
231
+ cr = self._current_range()
232
+ new_start = max(0, min(cr.start + n, b_len))
233
+ delta = new_start - cr.start
234
+ new_stop = max(0, min(cr.stop + delta, b_len))
235
+ self._range = range(new_start, new_stop, cr.step)
236
+ return self
237
+
238
+ # ------------------------------------------------------------------
239
+ # tolist / copy
240
+ # ------------------------------------------------------------------
241
+
242
+ def tolist(self) -> list:
243
+ """Return a new list containing the elements covered by this view."""
244
+ return list(self)
245
+
246
+ def copy(self) -> list:
247
+ """Alias for :meth:`tolist`."""
248
+ return self.tolist()
249
+
250
+ # ------------------------------------------------------------------
251
+ # base property
252
+ # ------------------------------------------------------------------
253
+
254
+ @property
255
+ def base(self):
256
+ """The underlying sequence this view points into."""
257
+ return self._base
258
+
259
+ # ------------------------------------------------------------------
260
+ # Internal helpers
261
+ # ------------------------------------------------------------------
262
+
263
+ def _setslice(self, sl: slice, values) -> None:
264
+ target_range = self._current_range()[sl]
265
+ values = list(values)
266
+
267
+ if abs(target_range.step) != 1:
268
+ # Extended slice assignment must match length exactly.
269
+ if len(values) != len(target_range):
270
+ raise ValueError(
271
+ f"attempt to assign sequence of size {len(values)} "
272
+ f"to extended slice of size {len(target_range)}"
273
+ )
274
+ for i, v in zip(target_range, values):
275
+ self._base[i] = v
276
+ else:
277
+ # Step ±1: delegate to the base (allows resizing if base supports it).
278
+ self._base[
279
+ slice(target_range.start, target_range.stop, target_range.step)
280
+ ] = values
@@ -0,0 +1,50 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "sliceview"
7
+ version = "0.1.0"
8
+ description = "Zero-copy, composable slice views for Python sequences"
9
+ readme = "README.md"
10
+ license = { text = "MIT" }
11
+ authors = [
12
+ { name = "Juliano Fischer Naves", email = "julianofischer@gmail.com" }
13
+ ]
14
+ keywords = ["slice", "view", "sequence", "zero-copy", "memory", "performance"]
15
+ classifiers = [
16
+ "Development Status :: 3 - Alpha",
17
+ "Intended Audience :: Developers",
18
+ "License :: OSI Approved :: MIT License",
19
+ "Programming Language :: Python :: 3",
20
+ "Programming Language :: Python :: 3.9",
21
+ "Programming Language :: Python :: 3.10",
22
+ "Programming Language :: Python :: 3.11",
23
+ "Programming Language :: Python :: 3.12",
24
+ "Programming Language :: Python :: 3.13",
25
+ "Topic :: Software Development :: Libraries",
26
+ "Topic :: Utilities",
27
+ ]
28
+ requires-python = ">=3.9"
29
+ dependencies = []
30
+
31
+ [project.optional-dependencies]
32
+ dev = [
33
+ "pytest>=7.0",
34
+ "pytest-cov",
35
+ ]
36
+
37
+ [project.urls]
38
+ Homepage = "https://github.com/julianofischer/sliceview"
39
+ "Bug Tracker" = "https://github.com/julianofischer/sliceview/issues"
40
+ Discussions = "https://discuss.python.org/t/slice-views-for-python-sequences/103531"
41
+
42
+ [tool.hatch.build.targets.wheel]
43
+ packages = ["src/sliceview"]
44
+
45
+ [tool.pytest.ini_options]
46
+ testpaths = ["tests"]
47
+ addopts = "-v --tb=short"
48
+
49
+ [tool.coverage.run]
50
+ source = ["src/sliceview"]
@@ -0,0 +1,317 @@
1
+ """Tests for sliceview."""
2
+
3
+ import pytest
4
+ from sliceview import sliceview
5
+
6
+
7
+ # ---------------------------------------------------------------------------
8
+ # Construction
9
+ # ---------------------------------------------------------------------------
10
+
11
+ class TestConstruction:
12
+ def test_basic(self):
13
+ sv = sliceview([1, 2, 3])
14
+ assert list(sv) == [1, 2, 3]
15
+
16
+ def test_with_start_stop(self):
17
+ sv = sliceview([0, 1, 2, 3, 4], 1, 4)
18
+ assert list(sv) == [1, 2, 3]
19
+
20
+ def test_with_step(self):
21
+ sv = sliceview([0, 1, 2, 3, 4], 0, 5, 2)
22
+ assert list(sv) == [0, 2, 4]
23
+
24
+ def test_with_slice_object(self):
25
+ sv = sliceview([0, 1, 2, 3, 4], slice(1, 4))
26
+ assert list(sv) == [1, 2, 3]
27
+
28
+ def test_negative_step(self):
29
+ sv = sliceview([0, 1, 2, 3, 4], 4, None, -1)
30
+ assert list(sv) == [4, 3, 2, 1, 0]
31
+
32
+ def test_invalid_base(self):
33
+ with pytest.raises(TypeError):
34
+ sliceview(42)
35
+
36
+ def test_string_base(self):
37
+ sv = sliceview("hello")[1:4]
38
+ assert list(sv) == ["e", "l", "l"]
39
+
40
+ def test_tuple_base(self):
41
+ sv = sliceview((10, 20, 30, 40))[::2]
42
+ assert list(sv) == [10, 30]
43
+
44
+
45
+ # ---------------------------------------------------------------------------
46
+ # Indexing
47
+ # ---------------------------------------------------------------------------
48
+
49
+ class TestIndexing:
50
+ def test_positive_index(self):
51
+ sv = sliceview([10, 20, 30])[:]
52
+ assert sv[0] == 10
53
+ assert sv[2] == 30
54
+
55
+ def test_negative_index(self):
56
+ sv = sliceview([10, 20, 30])
57
+ assert sv[-1] == 30
58
+ assert sv[-3] == 10
59
+
60
+ def test_out_of_range(self):
61
+ sv = sliceview([1, 2, 3])
62
+ with pytest.raises(IndexError):
63
+ _ = sv[10]
64
+
65
+ def test_index_into_strided_view(self):
66
+ sv = sliceview(list(range(10)))[::3]
67
+ assert sv[0] == 0
68
+ assert sv[1] == 3
69
+ assert sv[2] == 6
70
+
71
+
72
+ # ---------------------------------------------------------------------------
73
+ # Slicing (composition)
74
+ # ---------------------------------------------------------------------------
75
+
76
+ class TestSlicing:
77
+ def test_slice_returns_sliceview(self):
78
+ sv = sliceview([1, 2, 3, 4, 5])
79
+ assert isinstance(sv[1:3], sliceview)
80
+
81
+ def test_composed_slice(self):
82
+ data = list(range(20))
83
+ sv = sliceview(data)[2:][::3][1:4]
84
+ assert list(sv) == [5, 8, 11]
85
+
86
+ def test_no_copy_on_slice(self):
87
+ data = [1, 2, 3, 4, 5]
88
+ sv = sliceview(data)
89
+ sv2 = sv[1:4]
90
+ assert sv2.base is data
91
+
92
+ def test_full_slice_is_same_base(self):
93
+ data = [1, 2, 3]
94
+ sv = sliceview(data)
95
+ assert sv[:].base is data
96
+
97
+ def test_negative_step_composition(self):
98
+ data = list(range(10))
99
+ sv = sliceview(data)[::-1][::2]
100
+ assert list(sv) == [9, 7, 5, 3, 1]
101
+
102
+ def test_empty_slice(self):
103
+ sv = sliceview([1, 2, 3])[5:10]
104
+ assert list(sv) == []
105
+ assert len(sv) == 0
106
+
107
+
108
+ # ---------------------------------------------------------------------------
109
+ # Length
110
+ # ---------------------------------------------------------------------------
111
+
112
+ class TestLen:
113
+ def test_full(self):
114
+ assert len(sliceview([1, 2, 3])) == 3
115
+
116
+ def test_partial(self):
117
+ assert len(sliceview([1, 2, 3, 4, 5])[1:4]) == 3
118
+
119
+ def test_step(self):
120
+ assert len(sliceview(list(range(10)))[::3]) == 4
121
+
122
+ def test_empty(self):
123
+ assert len(sliceview([])) == 0
124
+
125
+ def test_reflects_base_mutation(self):
126
+ data = [1, 2, 3, 4, 5]
127
+ sv = sliceview(data)
128
+ assert len(sv) == 5
129
+ data.append(6)
130
+ assert len(sv) == 6
131
+
132
+
133
+ # ---------------------------------------------------------------------------
134
+ # Mutation (write-through)
135
+ # ---------------------------------------------------------------------------
136
+
137
+ class TestMutation:
138
+ def test_setitem_int(self):
139
+ data = [1, 2, 3, 4, 5]
140
+ sv = sliceview(data)[1:4]
141
+ sv[0] = 99
142
+ assert data == [1, 99, 3, 4, 5]
143
+
144
+ def test_setitem_slice(self):
145
+ data = list(range(5))
146
+ sv = sliceview(data)
147
+ sv[1:4] = [10, 20, 30]
148
+ assert data == [0, 10, 20, 30, 4]
149
+
150
+ def test_setitem_strided(self):
151
+ data = list(range(10))
152
+ sv = sliceview(data)[::2]
153
+ sv[0] = 99
154
+ assert data[0] == 99
155
+
156
+ def test_setitem_extended_slice_wrong_size(self):
157
+ data = list(range(10))
158
+ sv = sliceview(data)[::2]
159
+ with pytest.raises(ValueError):
160
+ sv[0:3] = [1, 2] # 3 slots, 2 values
161
+
162
+ def test_immutable_base_raises(self):
163
+ sv = sliceview((1, 2, 3))
164
+ with pytest.raises(TypeError):
165
+ sv[0] = 99
166
+
167
+
168
+ # ---------------------------------------------------------------------------
169
+ # Advance
170
+ # ---------------------------------------------------------------------------
171
+
172
+ class TestAdvance:
173
+ def test_advance_basic(self):
174
+ data = list(range(10))
175
+ sv = sliceview(data, 0, 3)
176
+ assert list(sv) == [0, 1, 2]
177
+ sv.advance(3)
178
+ assert list(sv) == [3, 4, 5]
179
+
180
+ def test_advance_returns_self(self):
181
+ sv = sliceview(list(range(10)), 0, 5)
182
+ assert sv.advance(5) is sv
183
+
184
+ def test_advance_past_end_clamps(self):
185
+ data = list(range(5))
186
+ sv = sliceview(data, 0, 3)
187
+ sv.advance(100)
188
+ assert list(sv) == []
189
+
190
+ def test_advance_negative(self):
191
+ data = list(range(10))
192
+ sv = sliceview(data, 5, 8)
193
+ sv.advance(-3)
194
+ assert list(sv) == [2, 3, 4]
195
+
196
+ def test_sliding_window(self):
197
+ data = list(range(12))
198
+ sv = sliceview(data, 0, 4)
199
+ result = []
200
+ for _ in range(3):
201
+ result.append(list(sv))
202
+ sv.advance(4)
203
+ assert result == [[0, 1, 2, 3], [4, 5, 6, 7], [8, 9, 10, 11]]
204
+
205
+
206
+ # ---------------------------------------------------------------------------
207
+ # Iteration
208
+ # ---------------------------------------------------------------------------
209
+
210
+ class TestIteration:
211
+ def test_iter(self):
212
+ sv = sliceview([10, 20, 30, 40])[1:3]
213
+ assert list(sv) == [20, 30]
214
+
215
+ def test_contains(self):
216
+ sv = sliceview([1, 2, 3, 4, 5])[1:4]
217
+ assert 2 in sv
218
+ assert 5 not in sv
219
+
220
+ def test_reversed(self):
221
+ sv = sliceview([1, 2, 3, 4, 5])
222
+ assert list(reversed(sv)) == [5, 4, 3, 2, 1]
223
+
224
+
225
+ # ---------------------------------------------------------------------------
226
+ # Equality and hashing
227
+ # ---------------------------------------------------------------------------
228
+
229
+ class TestEquality:
230
+ def test_equal_to_list(self):
231
+ sv = sliceview([1, 2, 3])
232
+ assert sv == [1, 2, 3]
233
+
234
+ def test_equal_to_sliceview(self):
235
+ data = [1, 2, 3, 4]
236
+ sv1 = sliceview(data)[1:3]
237
+ sv2 = sliceview(data)[1:3]
238
+ assert sv1 == sv2
239
+
240
+ def test_not_equal(self):
241
+ sv = sliceview([1, 2, 3])
242
+ assert sv != [1, 2, 4]
243
+
244
+ def test_unhashable(self):
245
+ sv = sliceview([1, 2, 3])
246
+ with pytest.raises(TypeError):
247
+ hash(sv)
248
+
249
+
250
+ # ---------------------------------------------------------------------------
251
+ # Repr
252
+ # ---------------------------------------------------------------------------
253
+
254
+ class TestRepr:
255
+ def test_repr_contains_slice(self):
256
+ sv = sliceview([1, 2, 3, 4])[1:3]
257
+ r = repr(sv)
258
+ assert "sliceview" in r
259
+ assert "1:3" in r
260
+
261
+ def test_repr_full(self):
262
+ sv = sliceview([1, 2, 3])
263
+ r = repr(sv)
264
+ assert "sliceview" in r
265
+
266
+
267
+ # ---------------------------------------------------------------------------
268
+ # tolist / copy
269
+ # ---------------------------------------------------------------------------
270
+
271
+ class TestCopy:
272
+ def test_tolist(self):
273
+ data = [1, 2, 3, 4, 5]
274
+ sv = sliceview(data)[1:4]
275
+ result = sv.tolist()
276
+ assert result == [2, 3, 4]
277
+ assert isinstance(result, list)
278
+ result[0] = 99
279
+ assert data[1] == 2 # original unchanged
280
+
281
+ def test_copy(self):
282
+ sv = sliceview([1, 2, 3])
283
+ assert sv.copy() == [1, 2, 3]
284
+
285
+
286
+ # ---------------------------------------------------------------------------
287
+ # base property
288
+ # ---------------------------------------------------------------------------
289
+
290
+ class TestBase:
291
+ def test_base_is_original(self):
292
+ data = [1, 2, 3]
293
+ sv = sliceview(data)
294
+ assert sv.base is data
295
+
296
+ def test_composed_base_is_original(self):
297
+ data = [1, 2, 3, 4, 5]
298
+ sv = sliceview(data)[1:][::2]
299
+ assert sv.base is data
300
+
301
+
302
+ # ---------------------------------------------------------------------------
303
+ # Live mutation reflection
304
+ # ---------------------------------------------------------------------------
305
+
306
+ class TestLiveMutation:
307
+ def test_write_through(self):
308
+ data = [1, 2, 3, 4, 5]
309
+ sv = sliceview(data)
310
+ data[2] = 99
311
+ assert sv[2] == 99
312
+
313
+ def test_append_reflected_in_len(self):
314
+ data = [1, 2, 3]
315
+ sv = sliceview(data)
316
+ data.append(4)
317
+ assert len(sv) == 4