1212
1313
1414class AlignedStream (io .RawIOBase ):
15- """Basic buffered stream that provides easy aligned reads.
15+ """Basic buffered stream that provides aligned reads.
1616
1717 Must be subclassed for various stream implementations. Subclasses can implement:
18- - _read(offset, length)
19- - _seek(pos, whence=io.SEEK_SET)
18+ - :meth:`~AlignedStream. _read`
19+ - :meth:`~AlignedStream._seek`
2020
21- The offset and length for _read are guaranteed to be aligned. The only time
22- that overriding _seek would make sense is if there's no known size of your stream,
23- but still want to provide SEEK_END functionality.
21+ The offset and length for ``_read`` are guaranteed to be aligned for streams of a known size.
22+ If your stream has an unknown size (i.e. ``size == None``), reads of length ``-1`` (i.e. read until EOF) will be
23+ passed through to your implementation of ``_read``.
24+ The only time that overriding ``_seek`` would make sense is if there's no known size of your stream,
25+ but still want to provide ``SEEK_END`` functionality.
2426
25- Most subclasses of AlignedStream take one or more file-like objects as source.
27+ Most subclasses of `` AlignedStream`` take one or more file-like objects as source.
2628 Operations on these subclasses, like reading, will modify the source file-like object as a side effect.
2729
2830 Args:
29- size: The size of the stream. This is used in read and seek operations. None if unknown.
31+ size: The size of the stream. This is used in read and seek operations. `` None`` if unknown.
3032 align: The alignment size. Read operations are aligned on this boundary. Also determines buffer size.
33+
34+ .. automethod:: _read
35+ .. automethod:: _seek
3136 """
3237
3338 def __init__ (self , size : int | None = None , align : int = STREAM_BUFFER_SIZE ):
@@ -39,26 +44,30 @@ def __init__(self, size: int | None = None, align: int = STREAM_BUFFER_SIZE):
3944 self ._pos_align = 0
4045
4146 self ._buf = None
42- self ._seek_lock = Lock ()
47+ self ._lock = Lock ()
4348
44- def _set_pos (self , pos : int ) -> None :
45- """Update the position and aligned position within the stream."""
46- new_pos_align = pos - ( pos % self . align )
49+ def readable (self ) -> bool :
50+ """Indicate that the stream is readable ."""
51+ return True
4752
48- if self . _pos_align != new_pos_align :
49- self . _pos_align = new_pos_align
50- self . _buf = None
53+ def seekable ( self ) -> bool :
54+ """Indicate that the stream is seekable."""
55+ return True
5156
52- self ._pos = pos
57+ def seek (self , pos : int , whence : int = io .SEEK_SET ) -> int :
58+ """Seek the stream to the specified position.
5359
54- def _fill_buf (self ) -> None :
55- """Fill the alignment buffer if we can."""
56- if self ._buf or (self .size is not None and (self .size <= self ._pos or self .size <= self ._pos_align )):
57- return
60+ Returns:
61+ The new stream position after seeking.
62+ """
63+ with self ._lock :
64+ pos = self ._seek (pos , whence )
65+ self ._set_pos (pos )
5866
59- self . _buf = self . _read ( self . _pos_align , self . align )
67+ return pos
6068
6169 def _seek (self , pos : int , whence : int = io .SEEK_SET ) -> int :
70+ """Calculate and return the new stream position after a seek."""
6271 if whence == io .SEEK_SET :
6372 if pos < 0 :
6473 raise ValueError (f"negative seek position { pos } " )
@@ -73,16 +82,32 @@ def _seek(self, pos: int, whence: int = io.SEEK_SET) -> int:
7382
7483 return pos
7584
76- def seek (self , pos : int , whence : int = io .SEEK_SET ) -> int :
77- """Seek the stream to the specified position."""
78- with self ._seek_lock :
79- pos = self ._seek (pos , whence )
80- self ._set_pos (pos )
85+ def _set_pos (self , pos : int ) -> None :
86+ """Update the position and aligned position within the stream."""
87+ new_pos_align = pos - (pos % self .align )
8188
82- return pos
89+ if self ._pos_align != new_pos_align :
90+ self ._pos_align = new_pos_align
91+ self ._buf = None
92+
93+ self ._pos = pos
94+
95+ def tell (self ) -> int :
96+ """Return current stream position."""
97+ return self ._pos
98+
99+ def _fill_buf (self ) -> None :
100+ """Fill the alignment buffer if we can."""
101+ if self ._buf or (self .size is not None and (self .size <= self ._pos or self .size <= self ._pos_align )):
102+ # Don't fill the buffer if:
103+ # - We already have a buffer
104+ # - The stream position is at the end (or beyond) the stream size
105+ return
106+
107+ self ._buf = self ._read (self ._pos_align , self .align )
83108
84109 def read (self , n : int = - 1 ) -> bytes :
85- """Read and return up to n bytes, or read to the end of the stream if n is -1 .
110+ """Read and return up to ``n`` bytes, or read to the end of the stream if ``n`` is ``-1`` .
86111
87112 Returns an empty bytes object on EOF.
88113 """
@@ -93,7 +118,7 @@ def read(self, n: int = -1) -> bytes:
93118 size = self .size
94119 align = self .align
95120
96- with self ._seek_lock :
121+ with self ._lock :
97122 if size is None and n == - 1 :
98123 r = []
99124 if self ._buf :
@@ -107,10 +132,12 @@ def read(self, n: int = -1) -> bytes:
107132 self ._set_pos (self ._pos + len (buf ))
108133 return buf
109134
135+ # If we know the stream size, adjust n
110136 if size is not None :
111137 remaining = size - self ._pos
112138 n = remaining if n == - 1 else min (n , remaining )
113139
140+ # Short path for when it turns out we don't need to read anything
114141 if n == 0 or (size is not None and size <= self ._pos ):
115142 return b""
116143
@@ -136,7 +163,7 @@ def read(self, n: int = -1) -> bytes:
136163
137164 self ._set_pos (self ._pos + read_len )
138165
139- # Misaligned end
166+ # Misaligned remaining bytes
140167 if n > 0 :
141168 self ._fill_buf ()
142169 r .append (self ._buf [:n ])
@@ -163,22 +190,28 @@ def readall(self) -> bytes:
163190 return self .read ()
164191
165192 def readoffset (self , offset : int , length : int ) -> bytes :
166- """Convenience method to read from a certain offset with 1 call."""
193+ """Convenience method to read from a given offset.
194+
195+ Args:
196+ offset: The offset in the stream to read from.
197+ length: The number of bytes to read.
198+ """
167199 self .seek (offset )
168200 return self .read (length )
169201
170- def tell (self ) -> int :
171- """Return current stream position."""
172- return self ._pos
202+ def peek (self , n : int ) -> bytes :
203+ """Convenience method to peek from the current offset without advancing the stream position.
173204
174- def close (self ) -> None :
175- pass
176-
177- def readable (self ) -> bool :
178- return True
205+ Args:
206+ n: The number of bytes to peek.
207+ """
208+ pos = self ._pos
209+ data = self .read (n )
210+ self ._set_pos (pos )
211+ return data
179212
180- def seekable (self ) -> bool :
181- return True
213+ def close (self ) -> None :
214+ """Close the stream. Does nothing by default."""
182215
183216
184217class RangeStream (AlignedStream ):
@@ -198,18 +231,25 @@ class RangeStream(AlignedStream):
198231 align: The alignment size.
199232 """
200233
201- def __init__ (self , fh : BinaryIO , offset : int , size : int , align : int = STREAM_BUFFER_SIZE ):
234+ def __init__ (self , fh : BinaryIO , offset : int , size : int | None , align : int = STREAM_BUFFER_SIZE ):
202235 super ().__init__ (size , align )
203236 self ._fh = fh
204237 self .offset = offset
205238
239+ def _seek (self , pos : int , whence : int = io .SEEK_SET ) -> int :
240+ if self .size is None and whence == io .SEEK_END :
241+ if (pos := self ._fh .seek (pos , whence )) is None :
242+ pos = self ._fh .tell ()
243+ return max (0 , pos - self .offset )
244+ return super ()._seek (pos , whence )
245+
206246 def _read (self , offset : int , length : int ) -> bytes :
207- read_length = min (length , self .size - offset )
247+ read_length = min (length , self .size - offset ) if self . size else length
208248 self ._fh .seek (self .offset + offset )
209249 return self ._fh .read (read_length )
210250
211251
212- class RelativeStream (AlignedStream ):
252+ class RelativeStream (RangeStream ):
213253 """Create a relative stream from another file-like object.
214254
215255 ASCII representation::
@@ -222,27 +262,12 @@ class RelativeStream(AlignedStream):
222262 Args:
223263 fh: The source file-like object.
224264 offset: The offset the stream should start from on the source file-like object.
225- size: The size the stream should be.
265+ size: Optional size the stream should be.
226266 align: The alignment size.
227267 """
228268
229269 def __init__ (self , fh : BinaryIO , offset : int , size : int | None = None , align : int = STREAM_BUFFER_SIZE ):
230- super ().__init__ (size , align )
231- self ._fh = fh
232- self .offset = offset
233-
234- def _seek (self , pos : int , whence : int = io .SEEK_SET ) -> int :
235- if whence == io .SEEK_END :
236- pos = self ._fh .seek (pos , whence )
237- if pos is None :
238- pos = self ._fh .tell ()
239- return max (0 , pos - self .offset )
240- return super ()._seek (pos , whence )
241-
242- def _read (self , offset : int , length : int ) -> bytes :
243- read_length = min (length , self .size - offset ) if self .size else length
244- self ._fh .seek (self .offset + offset )
245- return self ._fh .read (read_length )
270+ super ().__init__ (fh , offset , size , align )
246271
247272
248273class BufferedStream (RelativeStream ):
@@ -459,7 +484,7 @@ def add(self, offset: int, data: bytes | BinaryIO, size: int | None = None) -> N
459484
460485 Args:
461486 offset: The offset in bytes to add an overlay at.
462- data: The bytes or file-like object to overlay
487+ data: The bytes or file-like object to overlay.
463488 size: Optional size specification of the overlay, if it can't be inferred.
464489 """
465490 if not hasattr (data , "read" ):
0 commit comments