Skip to content

Commit

Permalink
Documented and tested active buffer (closes #31); fixed tail_mark-rel…
Browse files Browse the repository at this point in the history
…ative reading (closes #32)
  • Loading branch information
lmmx committed Aug 4, 2021
1 parent 6237d13 commit 015d8a0
Show file tree
Hide file tree
Showing 3 changed files with 70 additions and 15 deletions.
54 changes: 44 additions & 10 deletions src/range_streams/response.py
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,29 @@ def set_active_buf_range(self, rng: Range) -> None:

@property
def is_active_buf_range(self) -> bool:
"""
The active range is stored on the buffer the HTTP response stream writes to
(in the :attr:`~range_streams.response.RangeResponse._bytes.active_buf_range`
attribute) so that whenever the active range changes, it is detectable
immediately (all interfaces to read/seek/load the buffer are 'guarded' by a
call to :meth:`~range_streams.response.RangeResponse.buf_keep` to achieve this).
When this change is detected, since the cursor may be in another range of the
shared source buffer (where the previously active window was busy doing its
thing), the cursor is first moved to the last stored
:meth:`~range_streams.response.RangeResponse.tell` position, which is stored on
each :class:`~range_streams.response.RangeResponse` in the
:attr:`~range_streams.response.RangeResponse.told` attribute, and initialised as
``0`` so that on first use it simply refers to the start position of the window
range.
Note that the active range only changes for 'windowed'
:class:`~range_streams.response.RangeResponse` objects sharing a 'source' buffer
with a source :class:`~range_streams.response.RangeResponse in the
:attr:`~range_streams.stream.RangeStream._ranges` :class:`~ranges.RangeDict`.
To clarify: the active range changes on first use for non-windowed ranges, since
the active range is initialised as the empty range (but after that it doesn't!)
"""
return self._bytes.active_buf_range == self.request.range

@property
Expand Down Expand Up @@ -173,8 +196,12 @@ def buf_keep(self) -> None:
"""
if not self.is_active_buf_range:
rng = self.request.range
if DEBUG_VERBOSE:
print(f"Buffer switch... {rng=}")
if self.is_windowed:
cursor_dest = rng.start + self.told
if DEBUG_VERBOSE:
print(f"... {self.told=}")
self._bytes.seek(cursor_dest)
# Do not set `told` as it was just used (i.e. redundant to do so)
self.set_active_buf_range(rng=rng)
Expand Down Expand Up @@ -209,6 +236,9 @@ def prepare_reading_window(self):

@property
def client(self):
"""
The request's client.
"""
return self.request.client

@property
Expand Down Expand Up @@ -275,10 +305,12 @@ def tell(self) -> int:

def read(self, size=None):
"""
File-like reading within the range request stream.
File-like reading within the range request stream, with careful handling of
windowed ranges and tail marks.
"""
self.buf_keep()
tail_mark = self.tail_mark
if DEBUG_VERBOSE:
print(f"Reading {self.request.range}")
# ...
if not self.read_ready:
# Only run on the first use after init
Expand All @@ -296,12 +328,15 @@ def read(self, size=None):
# Rewind the cursor to the start position now the bytes to read are loaded
self._bytes.seek(left_off_at)
if self.is_windowed:
# Would need to offset this if source range is non-total range
# (also may need to take into account tail-mark for windows?)
window_end = self.request.range.end
# Convert absolute window end to relative offset on source range
# (should do this using window_offset to permit non-total ranges!)
window_end = self.request.range.end - self.tail_mark
remaining_bytes = window_end - left_off_at
if size is None or size > remaining_bytes:
size = remaining_bytes
else:
rng_len = self.total_len_to_read
remaining_bytes = rng_len - left_off_at
if size is None or size > remaining_bytes:
size = remaining_bytes
read_bytes = self._bytes.read(size)
self.store_tell()
return read_bytes
Expand All @@ -320,7 +355,7 @@ def seek(self, position, whence=SEEK_SET):

@property
def total_len_to_read(self):
return range_len(self.request.range) + 1
return range_len(self.request.range) + 1 - self.tail_mark

def is_consumed(self) -> bool:
"""
Expand Down Expand Up @@ -348,7 +383,6 @@ def is_consumed(self) -> bool:
:meth:`~range_streams.stream.RangeStream.burn_range` and
:meth:`~range_streams.stream.RangeStream.handle_overlap`).
"""
tail_mark = self.tail_mark
if not self.is_in_window:
# File cursor position may not be set to the start of the window but when it
# is read it will be placed at the start (don't do this here: checking if
Expand All @@ -357,5 +391,5 @@ def is_consumed(self) -> bool:
read_so_far = 0
else:
read_so_far = self.tell()
len_to_read = self.total_len_to_read - tail_mark
len_to_read = self.total_len_to_read
return (len_to_read - read_so_far) <= 0 # should not go below!
12 changes: 7 additions & 5 deletions tests/overlaps_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -241,10 +241,10 @@ def test_partial_overlap_multiple_ranges(
assert len(external_rng_list) == 3


@mark.parametrize("initial_range", [Range(3, 7)])
@mark.parametrize("overlapping_range,expected", [(Range(5, 8), b"\x02\x03")])
@mark.parametrize("initial_range,expected1", [(Range(3, 7), b"\x02\x03")])
@mark.parametrize("overlapping_range,expected2", [(Range(5, 8), b"\x04\x05\x06")])
def test_overlapped_read(
empty_range_stream_fresh, initial_range, overlapping_range, expected
empty_range_stream_fresh, initial_range, overlapping_range, expected1, expected2
):
"""
Partial overlap with tail of the centred range ``[3,7)`` covered on one range
Expand All @@ -254,5 +254,7 @@ def test_overlapped_read(
stream = empty_range_stream_fresh
stream.add(byte_range=initial_range)
stream.add(byte_range=overlapping_range)
b = stream._ranges[initial_range.start].read()
assert b == expected
b1 = stream.ranges[initial_range.start].read()
assert b1 == expected1
b2 = stream.ranges[overlapping_range.start].read()
assert b2 == expected2
19 changes: 19 additions & 0 deletions tests/range_stream_mono_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -154,3 +154,22 @@ def test_correct_window_read_all(monostream_fresh, start1, stop1, read1, expecte
assert monostream_fresh.active_range_response.told == read1
assert monostream_fresh.tell() == read1
assert len(monostream_fresh.read()) == 0


@mark.parametrize("initial_range,expected1", [(Range(3, 7), b"\x02\x03")])
@mark.parametrize("overlapping_range,expected2", [(Range(5, 8), b"\x04\x05\x06")])
def test_overlapped_read(
monostream_fresh, initial_range, overlapping_range, expected1, expected2
):
"""
Partial overlap with tail of the centred range ``[3,7)`` covered on one range
``[5,8)`` should increment the tail mark so ``[3,7)`` is reduced to ``[3,5)``
and subsequently when read it should only give two bytes rather than four.
"""
stream = monostream_fresh
stream.add(byte_range=initial_range)
stream.add(byte_range=overlapping_range)
b1 = stream.ranges[initial_range.start].read()
assert b1 == expected1
b2 = stream.ranges[overlapping_range.start].read()
assert b2 == expected2

0 comments on commit 015d8a0

Please sign in to comment.