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.
- sliceview-0.1.0/LICENSE +21 -0
- sliceview-0.1.0/PKG-INFO +153 -0
- sliceview-0.1.0/README.md +125 -0
- sliceview-0.1.0/__init__.py +280 -0
- sliceview-0.1.0/pyproject.toml +50 -0
- sliceview-0.1.0/test_sliceview.py +317 -0
sliceview-0.1.0/LICENSE
ADDED
|
@@ -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.
|
sliceview-0.1.0/PKG-INFO
ADDED
|
@@ -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
|