progressive_io 2.0.2 → 2.1.0
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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +15 -0
- data/README.md +12 -2
- data/lib/progressive_io.rb +236 -75
- data/test/test_progressive_io.rb +153 -11
- metadata +4 -4
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 384d400209c61ff65f9298a602c53b8b6ce6cdf837d56b58a6e4cb900e932377
|
|
4
|
+
data.tar.gz: 0e68bbc55b01864b300a451496ba3c4e86839ec4f34bfb7d2f7cec9cb47fb415
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 43468779c7d211dbacacb79918c4fb719f100c67fb1cfaf31a881ba82cf1b8681aa21e48dd0ee34d90cfda2c74d06e29410813245f0f1c4fc300cab8d5c24fc9
|
|
7
|
+
data.tar.gz: e1a5cf367640e3bfb6b97ec527ff2cb570a76b06425fd8df644d83f8beb7f2db5ca335c03762f473b97c70840604ee1ce9010bd321dd9b722393fa94f2e4bffa
|
data/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,18 @@
|
|
|
1
|
+
## 2.1.0
|
|
2
|
+
|
|
3
|
+
* Report `readpartial`, `read_nonblock`, `sysread`, `getbyte`, `readbyte`, `each_char` and
|
|
4
|
+
`each_codepoint`, which went past the wrapper unreported.
|
|
5
|
+
* `IO.copy_stream` reports now. It read a wrapped File by opening its path itself, and a wrapped
|
|
6
|
+
IO through `to_io`, so the wrapper no longer passes on `to_path` or `to_io` - reach the
|
|
7
|
+
wrapped IO with `__getobj__` where you need it (`IO.select`, say). Without them
|
|
8
|
+
`IO.copy_stream` reads through `readpartial`, which also means no zero-copy fast path.
|
|
9
|
+
* Pass keyword arguments on: `gets(chomp: true)`, `readline`, `readlines`, `each` and
|
|
10
|
+
`each_line` used to raise `TypeError`.
|
|
11
|
+
* Work with pipes and sockets. Every read raised `Errno::ESPIPE`, since reporting asked the IO
|
|
12
|
+
for its position; for a stream without one, the bytes read so far are reported instead, and
|
|
13
|
+
`ungetc`/`ungetbyte` take pushed-back bytes off that count.
|
|
14
|
+
* Require Ruby 3.2.
|
|
15
|
+
|
|
1
16
|
## 2.0.2
|
|
2
17
|
|
|
3
18
|
* Fix `each_line` to return an Enumerator that properly references `:each_line` method
|
data/README.md
CHANGED
|
@@ -8,7 +8,9 @@ A wrapper for IO objects that allows a callback to be set which is called when a
|
|
|
8
8
|
|
|
9
9
|
## FEATURES/PROBLEMS
|
|
10
10
|
|
|
11
|
-
* Wraps any IO
|
|
11
|
+
* Wraps any IO: files, StringIO, pipes and sockets
|
|
12
|
+
* Reports every kind of read, including `readpartial`, so `IO.copy_stream` reports too
|
|
13
|
+
* On a pipe or a socket, which have no position, reports the bytes read so far instead
|
|
12
14
|
|
|
13
15
|
## SYNOPSIS
|
|
14
16
|
|
|
@@ -42,9 +44,17 @@ io_with_progress.each do | line | # Each yielded line will call the callback blo
|
|
|
42
44
|
end
|
|
43
45
|
```
|
|
44
46
|
|
|
47
|
+
Copying reports as it goes:
|
|
48
|
+
|
|
49
|
+
```ruby
|
|
50
|
+
IO.copy_stream(ProgressiveIO.new(File.open("/bigfile.dat")) { |pos| pbar.set(pos) }, destination)
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
The wrapper does not pass on `to_io` or `to_path`, since `IO.copy_stream` would use them to read the file past it. To get at the wrapped IO, use `__getobj__`.
|
|
54
|
+
|
|
45
55
|
## REQUIREMENTS
|
|
46
56
|
|
|
47
|
-
* Ruby
|
|
57
|
+
* Ruby 3.2+
|
|
48
58
|
|
|
49
59
|
## INSTALL
|
|
50
60
|
|
data/lib/progressive_io.rb
CHANGED
|
@@ -3,45 +3,52 @@
|
|
|
3
3
|
require 'delegate'
|
|
4
4
|
|
|
5
5
|
# A wrapper class that provides progress tracking for IO operations.
|
|
6
|
-
#
|
|
6
|
+
#
|
|
7
7
|
# This class wraps an IO object and calls a progress block whenever data is read,
|
|
8
8
|
# allowing you to track reading progress for operations like file uploads or downloads.
|
|
9
|
-
#
|
|
9
|
+
#
|
|
10
|
+
# The block receives the position after each read. A pipe, a socket or a TTY has no position,
|
|
11
|
+
# so for those it receives the number of bytes read through the wrapper so far instead.
|
|
12
|
+
#
|
|
10
13
|
# @example Basic usage with a file
|
|
11
14
|
# file = File.open('large_file.txt')
|
|
12
15
|
# progress_io = ProgressiveIO.new(file) do |current_pos|
|
|
13
16
|
# puts "Read #{current_pos} bytes"
|
|
14
17
|
# end
|
|
15
|
-
#
|
|
18
|
+
#
|
|
16
19
|
# progress_io.each_line do |line|
|
|
17
20
|
# # Process each line
|
|
18
21
|
# end
|
|
19
|
-
#
|
|
22
|
+
#
|
|
20
23
|
# @example Usage with a StringIO
|
|
21
24
|
# string_io = StringIO.new("Hello\nWorld\n")
|
|
22
25
|
# progress_io = ProgressiveIO.new(string_io) do |current_pos|
|
|
23
26
|
# puts "Read #{current_pos} bytes"
|
|
24
27
|
# end
|
|
25
|
-
#
|
|
28
|
+
#
|
|
26
29
|
# content = progress_io.read
|
|
27
|
-
#
|
|
30
|
+
#
|
|
31
|
+
# @example Copying, which reads through readpartial
|
|
32
|
+
# IO.copy_stream(ProgressiveIO.new(source) { |pos| bar.update(pos) }, destination)
|
|
33
|
+
#
|
|
28
34
|
# @since 2.0.0
|
|
29
35
|
class ProgressiveIO < SimpleDelegator
|
|
30
36
|
# The version of the ProgressiveIO library
|
|
31
|
-
VERSION = '2.0
|
|
32
|
-
|
|
37
|
+
VERSION = '2.1.0'
|
|
38
|
+
|
|
33
39
|
# @return [Proc, nil] The progress callback block that will be called when data is read
|
|
34
40
|
# The block receives one parameter: current position
|
|
35
41
|
attr_accessor :progress_block
|
|
36
|
-
|
|
42
|
+
|
|
37
43
|
# Creates a new ProgressiveIO wrapper around an IO object.
|
|
38
|
-
#
|
|
39
|
-
# @param with_io [IO] The IO object to wrap (File, StringIO, etc.)
|
|
44
|
+
#
|
|
45
|
+
# @param with_io [IO] The IO object to wrap (File, StringIO, a pipe, etc.)
|
|
40
46
|
# @param blk [Proc, nil] Optional block that will be called when data is read
|
|
41
47
|
# The block receives one parameter: current position
|
|
42
48
|
# @yield [current_pos] The progress callback
|
|
43
|
-
# @yieldparam current_pos [Integer] The current position in the IO stream
|
|
44
|
-
#
|
|
49
|
+
# @yieldparam current_pos [Integer] The current position in the IO stream, or the bytes
|
|
50
|
+
# read so far for a stream that has no position
|
|
51
|
+
#
|
|
45
52
|
# @example
|
|
46
53
|
# file = File.open('data.txt')
|
|
47
54
|
# progress_io = ProgressiveIO.new(file) do |pos|
|
|
@@ -50,170 +57,324 @@ class ProgressiveIO < SimpleDelegator
|
|
|
50
57
|
def initialize(with_io, &blk)
|
|
51
58
|
super(with_io)
|
|
52
59
|
@progress_block = blk.to_proc if blk
|
|
60
|
+
@positioned = nil
|
|
61
|
+
@consumed = 0
|
|
53
62
|
end
|
|
54
|
-
|
|
63
|
+
|
|
55
64
|
# Iterates over the IO stream line by line, calling the progress block for each line.
|
|
56
|
-
#
|
|
65
|
+
#
|
|
57
66
|
# @param args [Array] Arguments to pass to the underlying IO#each method (separator and/or limit)
|
|
67
|
+
# @param kwargs [Hash] Keyword arguments to pass on, such as `chomp: true`
|
|
58
68
|
# @yield [line] Each line from the IO stream
|
|
59
69
|
# @yieldparam line [String] A line from the IO stream
|
|
60
70
|
# @return [Enumerator] An enumerator if no block is given
|
|
61
|
-
#
|
|
71
|
+
#
|
|
62
72
|
# @example
|
|
63
73
|
# progress_io.each do |line|
|
|
64
74
|
# puts "Processing: #{line.chomp}"
|
|
65
75
|
# end
|
|
66
|
-
#
|
|
76
|
+
#
|
|
67
77
|
# @example Using as an Enumerator
|
|
68
78
|
# progress_io.each.with_index do |line, idx|
|
|
69
79
|
# puts "Line #{idx}: #{line.chomp}"
|
|
70
80
|
# end
|
|
71
|
-
def each(*args, &blk)
|
|
72
|
-
return enum_for(__method__, *args) unless block_given?
|
|
73
|
-
|
|
81
|
+
def each(*args, **kwargs, &blk)
|
|
82
|
+
return enum_for(__method__, *args, **kwargs) unless block_given?
|
|
83
|
+
|
|
74
84
|
# Report offset at each call of the iterator
|
|
75
|
-
super(*args) do |line|
|
|
76
|
-
yield(line).tap { notify_read }
|
|
85
|
+
super(*args, **kwargs) do |line|
|
|
86
|
+
yield(line).tap { notify_read(line, chomped: kwargs[:chomp]) }
|
|
77
87
|
end
|
|
78
88
|
end
|
|
79
|
-
|
|
89
|
+
|
|
80
90
|
# Iterates over the IO stream line by line, calling the progress block for each line.
|
|
81
91
|
# This is an alias-like method for {#each} that ensures proper Enumerator behavior.
|
|
82
|
-
#
|
|
92
|
+
#
|
|
83
93
|
# @param args [Array] Arguments to pass to the underlying IO#each_line method (separator and/or limit)
|
|
94
|
+
# @param kwargs [Hash] Keyword arguments to pass on, such as `chomp: true`
|
|
84
95
|
# @yield [line] Each line from the IO stream
|
|
85
96
|
# @yieldparam line [String] A line from the IO stream
|
|
86
97
|
# @return [Enumerator] An enumerator if no block is given
|
|
87
|
-
#
|
|
98
|
+
#
|
|
88
99
|
# @example
|
|
89
100
|
# progress_io.each_line do |line|
|
|
90
101
|
# puts "Processing: #{line.chomp}"
|
|
91
102
|
# end
|
|
92
|
-
#
|
|
103
|
+
#
|
|
93
104
|
# @example Using as an Enumerator
|
|
94
105
|
# progress_io.each_line.with_index do |line, idx|
|
|
95
106
|
# puts "Line #{idx}: #{line.chomp}"
|
|
96
107
|
# end
|
|
97
|
-
def each_line(*args, &blk)
|
|
98
|
-
return enum_for(__method__, *args) unless block_given?
|
|
99
|
-
|
|
100
|
-
each(*args, &blk)
|
|
108
|
+
def each_line(*args, **kwargs, &blk)
|
|
109
|
+
return enum_for(__method__, *args, **kwargs) unless block_given?
|
|
110
|
+
|
|
111
|
+
each(*args, **kwargs, &blk)
|
|
101
112
|
end
|
|
102
|
-
|
|
113
|
+
|
|
103
114
|
# Iterates over the IO stream byte by byte, calling the progress block for each byte.
|
|
104
|
-
#
|
|
115
|
+
#
|
|
105
116
|
# @yield [byte] Each byte from the IO stream
|
|
106
117
|
# @yieldparam byte [Integer] A byte from the IO stream (0-255)
|
|
107
118
|
# @return [Enumerator] An enumerator if no block is given
|
|
108
|
-
#
|
|
119
|
+
#
|
|
109
120
|
# @example
|
|
110
121
|
# progress_io.each_byte do |byte|
|
|
111
122
|
# puts "Byte: #{byte}"
|
|
112
123
|
# end
|
|
113
|
-
#
|
|
124
|
+
#
|
|
114
125
|
# @example Using as an Enumerator
|
|
115
126
|
# progress_io.each_byte.with_index do |byte, idx|
|
|
116
127
|
# puts "Byte #{idx}: #{byte}"
|
|
117
128
|
# end
|
|
118
129
|
def each_byte(&blk)
|
|
119
130
|
return enum_for(__method__) unless block_given?
|
|
120
|
-
|
|
131
|
+
|
|
121
132
|
# Report offset at each call of the iterator
|
|
122
|
-
super { |b| yield(b).tap { notify_read } }
|
|
133
|
+
super { |b| yield(b).tap { notify_read(b) } }
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
# Iterates over the IO stream character by character, calling the progress block for each.
|
|
137
|
+
#
|
|
138
|
+
# @yield [char] Each character from the IO stream
|
|
139
|
+
# @yieldparam char [String] A character from the IO stream
|
|
140
|
+
# @return [Enumerator] An enumerator if no block is given
|
|
141
|
+
# @since 2.1.0
|
|
142
|
+
def each_char(&blk)
|
|
143
|
+
return enum_for(__method__) unless block_given?
|
|
144
|
+
|
|
145
|
+
super { |c| yield(c).tap { notify_read(c) } }
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
# Iterates over the IO stream codepoint by codepoint, calling the progress block for each.
|
|
149
|
+
#
|
|
150
|
+
# @yield [codepoint] Each codepoint from the IO stream
|
|
151
|
+
# @yieldparam codepoint [Integer] A codepoint from the IO stream
|
|
152
|
+
# @return [Enumerator] An enumerator if no block is given
|
|
153
|
+
# @since 2.1.0
|
|
154
|
+
def each_codepoint(&blk)
|
|
155
|
+
return enum_for(__method__) unless block_given?
|
|
156
|
+
|
|
157
|
+
super { |c| yield(c).tap { notify_read(codepoint_string(c)) } }
|
|
123
158
|
end
|
|
124
|
-
|
|
159
|
+
|
|
125
160
|
# Reads a single character from the IO stream.
|
|
126
|
-
#
|
|
161
|
+
#
|
|
127
162
|
# @return [String, nil] The next character or nil if at end of stream
|
|
128
163
|
# @see IO#getc
|
|
129
164
|
def getc
|
|
130
|
-
super.tap { notify_read }
|
|
165
|
+
super.tap { |c| notify_read(c) }
|
|
131
166
|
end
|
|
132
|
-
|
|
167
|
+
|
|
168
|
+
# Reads a single byte from the IO stream.
|
|
169
|
+
#
|
|
170
|
+
# @return [Integer, nil] The next byte or nil if at end of stream
|
|
171
|
+
# @see IO#getbyte
|
|
172
|
+
# @since 2.1.0
|
|
173
|
+
def getbyte
|
|
174
|
+
super.tap { |b| notify_read(b) }
|
|
175
|
+
end
|
|
176
|
+
|
|
133
177
|
# Reads a line from the IO stream.
|
|
134
|
-
#
|
|
178
|
+
#
|
|
135
179
|
# @param args [Array] Arguments to pass to the underlying IO#gets method
|
|
180
|
+
# @param kwargs [Hash] Keyword arguments to pass on, such as `chomp: true`
|
|
136
181
|
# @return [String, nil] The next line or nil if at end of stream
|
|
137
182
|
# @see IO#gets
|
|
138
|
-
def gets(*args)
|
|
139
|
-
super(*args).tap { notify_read }
|
|
183
|
+
def gets(*args, **kwargs)
|
|
184
|
+
super(*args, **kwargs).tap { |line| notify_read(line, chomped: kwargs[:chomp]) }
|
|
140
185
|
end
|
|
141
|
-
|
|
186
|
+
|
|
142
187
|
# Reads data from the IO stream.
|
|
143
|
-
#
|
|
188
|
+
#
|
|
144
189
|
# @param a [Array] Arguments to pass to the underlying IO#read method
|
|
145
190
|
# @return [String, nil] The read data or nil if at end of stream
|
|
146
191
|
# @see IO#read
|
|
147
192
|
def read(*a)
|
|
148
|
-
super(*a).tap { notify_read }
|
|
193
|
+
super(*a).tap { |data| notify_read(data) }
|
|
149
194
|
end
|
|
150
|
-
|
|
195
|
+
|
|
196
|
+
# Reads at most a number of bytes, blocking only if none are available. IO.copy_stream reads
|
|
197
|
+
# through this when its source is not an IO itself.
|
|
198
|
+
#
|
|
199
|
+
# @param a [Array] Arguments to pass to the underlying IO#readpartial method
|
|
200
|
+
# @return [String] The read data
|
|
201
|
+
# @raise [EOFError] If at end of stream
|
|
202
|
+
# @see IO#readpartial
|
|
203
|
+
# @since 2.1.0
|
|
204
|
+
def readpartial(*a)
|
|
205
|
+
super(*a).tap { |data| notify_read(data) }
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
# Reads at most a number of bytes without blocking.
|
|
209
|
+
#
|
|
210
|
+
# @param a [Array] Arguments to pass to the underlying IO#read_nonblock method
|
|
211
|
+
# @param kwargs [Hash] Keyword arguments to pass on, such as `exception: false`
|
|
212
|
+
# @return [String, Symbol, nil] The read data, or `:wait_readable` with `exception: false`
|
|
213
|
+
# @see IO#read_nonblock
|
|
214
|
+
# @since 2.1.0
|
|
215
|
+
def read_nonblock(*a, **kwargs)
|
|
216
|
+
super(*a, **kwargs).tap { |data| notify_read(data) }
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
# Reads bytes with a low-level read, bypassing buffering.
|
|
220
|
+
#
|
|
221
|
+
# @param a [Array] Arguments to pass to the underlying IO#sysread method
|
|
222
|
+
# @return [String] The read data
|
|
223
|
+
# @raise [EOFError] If at end of stream
|
|
224
|
+
# @see IO#sysread
|
|
225
|
+
# @since 2.1.0
|
|
226
|
+
def sysread(*a)
|
|
227
|
+
super(*a).tap { |data| notify_read(data) }
|
|
228
|
+
end
|
|
229
|
+
|
|
151
230
|
# Reads a specific number of bytes from the IO stream.
|
|
152
|
-
#
|
|
231
|
+
#
|
|
153
232
|
# @param a [Array] Arguments to pass to the underlying IO#readbytes method
|
|
154
233
|
# @return [String] The read bytes
|
|
155
234
|
# @see IO#readbytes
|
|
156
235
|
def readbytes(*a)
|
|
157
|
-
super(*a).tap { notify_read }
|
|
236
|
+
super(*a).tap { |data| notify_read(data) }
|
|
237
|
+
end
|
|
238
|
+
|
|
239
|
+
# Reads a single byte from the IO stream.
|
|
240
|
+
#
|
|
241
|
+
# @return [Integer] The next byte
|
|
242
|
+
# @raise [EOFError] If at end of stream
|
|
243
|
+
# @see IO#readbyte
|
|
244
|
+
# @since 2.1.0
|
|
245
|
+
def readbyte
|
|
246
|
+
super.tap { |b| notify_read(b) }
|
|
158
247
|
end
|
|
159
|
-
|
|
248
|
+
|
|
160
249
|
# Reads a single character from the IO stream.
|
|
161
|
-
#
|
|
250
|
+
#
|
|
162
251
|
# @return [String] The next character
|
|
163
252
|
# @raise [EOFError] If at end of stream
|
|
164
253
|
# @see IO#readchar
|
|
165
254
|
def readchar
|
|
166
|
-
super.tap { notify_read }
|
|
255
|
+
super.tap { |c| notify_read(c) }
|
|
167
256
|
end
|
|
168
|
-
|
|
257
|
+
|
|
169
258
|
# Reads a line from the IO stream.
|
|
170
|
-
#
|
|
259
|
+
#
|
|
171
260
|
# @param a [Array] Arguments to pass to the underlying IO#readline method
|
|
261
|
+
# @param kwargs [Hash] Keyword arguments to pass on, such as `chomp: true`
|
|
172
262
|
# @return [String] The next line
|
|
173
263
|
# @raise [EOFError] If at end of stream
|
|
174
264
|
# @see IO#readline
|
|
175
|
-
def readline(*a)
|
|
176
|
-
super(*a).tap { notify_read }
|
|
265
|
+
def readline(*a, **kwargs)
|
|
266
|
+
super(*a, **kwargs).tap { |line| notify_read(line, chomped: kwargs[:chomp]) }
|
|
177
267
|
end
|
|
178
|
-
|
|
268
|
+
|
|
179
269
|
# Reads all lines from the IO stream.
|
|
180
|
-
#
|
|
270
|
+
#
|
|
181
271
|
# @param a [Array] Arguments to pass to the underlying IO#readlines method
|
|
272
|
+
# @param kwargs [Hash] Keyword arguments to pass on, such as `chomp: true`
|
|
182
273
|
# @return [Array<String>] Array of lines
|
|
183
274
|
# @see IO#readlines
|
|
184
|
-
def readlines(*a)
|
|
185
|
-
super(*a).tap { notify_read }
|
|
275
|
+
def readlines(*a, **kwargs)
|
|
276
|
+
super(*a, **kwargs).tap { |lines| notify_read(lines, chomped: kwargs[:chomp]) }
|
|
277
|
+
end
|
|
278
|
+
|
|
279
|
+
# Pushes a character back onto the stream. Not a read, so it is not reported - but on a stream
|
|
280
|
+
# without a position it is taken off the bytes read, so it is not counted twice.
|
|
281
|
+
#
|
|
282
|
+
# @param c [String, Integer] The character to push back
|
|
283
|
+
# @return [nil]
|
|
284
|
+
# @see IO#ungetc
|
|
285
|
+
# @since 2.1.0
|
|
286
|
+
def ungetc(c)
|
|
287
|
+
super(c).tap { unread(c) }
|
|
288
|
+
end
|
|
289
|
+
|
|
290
|
+
# Pushes a byte (or bytes) back onto the stream, see {#ungetc}.
|
|
291
|
+
#
|
|
292
|
+
# @param b [String, Integer] The byte or bytes to push back
|
|
293
|
+
# @return [nil]
|
|
294
|
+
# @see IO#ungetbyte
|
|
295
|
+
# @since 2.1.0
|
|
296
|
+
def ungetbyte(b)
|
|
297
|
+
super(b).tap { unread(b) }
|
|
298
|
+
end
|
|
299
|
+
|
|
300
|
+
# Conversions the wrapper does not pass on. Given `to_path`, IO.copy_stream opens the file by
|
|
301
|
+
# name itself, and given `to_io` (IO.select and the like too) it reads the wrapped IO directly
|
|
302
|
+
# - past the wrapper either way, with nothing reported. Without them IO.copy_stream reads
|
|
303
|
+
# through {#readpartial}. To get at the wrapped IO, use `__getobj__`.
|
|
304
|
+
#
|
|
305
|
+
# @since 2.1.0
|
|
306
|
+
WITHHELD = [:to_io, :to_path].freeze
|
|
307
|
+
|
|
308
|
+
def respond_to_missing?(name, include_private = false)
|
|
309
|
+
!WITHHELD.include?(name) && super
|
|
186
310
|
end
|
|
187
|
-
|
|
311
|
+
|
|
312
|
+
# Keyword arguments reach the wrapped IO through here, as they do in Delegator's own
|
|
313
|
+
ruby2_keywords def method_missing(name, *args, &blk)
|
|
314
|
+
raise NoMethodError.new("ProgressiveIO does not hand out the IO it wraps, so that reads go through it - use __getobj__", name) if WITHHELD.include?(name)
|
|
315
|
+
super
|
|
316
|
+
end
|
|
317
|
+
|
|
188
318
|
# Seeks to a position in the IO stream.
|
|
189
|
-
#
|
|
319
|
+
#
|
|
190
320
|
# @param a [Array] Arguments to pass to the underlying IO#seek method
|
|
191
321
|
# @return [Integer] The new position
|
|
192
322
|
# @see IO#seek
|
|
193
323
|
def seek(*a)
|
|
194
324
|
super(*a)
|
|
195
325
|
end
|
|
196
|
-
|
|
197
|
-
# def ungetc(*a)
|
|
198
|
-
# super(*a).tap { notify_read }
|
|
199
|
-
# end
|
|
200
|
-
|
|
326
|
+
|
|
201
327
|
# Sets the position in the IO stream.
|
|
202
|
-
#
|
|
328
|
+
#
|
|
203
329
|
# @param p [Integer] The new position
|
|
204
330
|
# @return [Integer] The new position
|
|
205
331
|
# @see IO#pos=
|
|
206
332
|
def pos=(p)
|
|
207
333
|
super(p).tap { notify_read }
|
|
208
334
|
end
|
|
209
|
-
|
|
335
|
+
|
|
210
336
|
private
|
|
211
|
-
|
|
212
|
-
# Calls the progress block with current position.
|
|
213
|
-
#
|
|
214
|
-
#
|
|
337
|
+
|
|
338
|
+
# Calls the progress block with the current position. A stream without one (a pipe, a
|
|
339
|
+
# socket) raises when asked for it, and from then on the bytes read so far stand in.
|
|
340
|
+
#
|
|
341
|
+
# @param data [String, Integer, Array, nil] What the read returned
|
|
342
|
+
# @param chomped [Boolean] Whether the read took line separators off what it returned, which
|
|
343
|
+
# still came off the stream
|
|
215
344
|
# @return [void]
|
|
216
|
-
def notify_read
|
|
217
|
-
|
|
345
|
+
def notify_read(data = nil, chomped: false)
|
|
346
|
+
position = positioned? ? pos : (@consumed += bytes_in(data, chomped))
|
|
347
|
+
@progress_block.call(position) if @progress_block
|
|
348
|
+
end
|
|
349
|
+
|
|
350
|
+
def positioned?
|
|
351
|
+
return @positioned unless @positioned.nil?
|
|
352
|
+
pos
|
|
353
|
+
@positioned = true
|
|
354
|
+
rescue Errno::ESPIPE, IOError
|
|
355
|
+
@positioned = false
|
|
356
|
+
end
|
|
357
|
+
|
|
358
|
+
def unread(data)
|
|
359
|
+
@consumed = [@consumed - bytes_in(data, false), 0].max unless positioned?
|
|
360
|
+
end
|
|
361
|
+
|
|
362
|
+
# With chomp, a line came off the stream with a separator that is not in it anymore. Which
|
|
363
|
+
# one is not known here; "\n" is the default and by far the likeliest, and the last line
|
|
364
|
+
# of a stream may have had none - so on a stream without a position, a chomped count can
|
|
365
|
+
# be a byte off per line, and the last one high by one
|
|
366
|
+
def bytes_in(data, chomped)
|
|
367
|
+
case data
|
|
368
|
+
when String then data.bytesize + (chomped ? 1 : 0)
|
|
369
|
+
when Integer then 1
|
|
370
|
+
when Array then data.sum { |item| bytes_in(item, chomped) }
|
|
371
|
+
else 0
|
|
372
|
+
end
|
|
373
|
+
end
|
|
374
|
+
|
|
375
|
+
def codepoint_string(codepoint)
|
|
376
|
+
codepoint.chr(external_encoding || Encoding::UTF_8)
|
|
377
|
+
rescue RangeError, EncodingError, NoMethodError
|
|
378
|
+
"?"
|
|
218
379
|
end
|
|
219
380
|
end
|
data/test/test_progressive_io.rb
CHANGED
|
@@ -3,6 +3,7 @@ require_relative "../lib/progressive_io"
|
|
|
3
3
|
require "flexmock"
|
|
4
4
|
require "flexmock/minitest"
|
|
5
5
|
require "stringio"
|
|
6
|
+
require "tempfile"
|
|
6
7
|
|
|
7
8
|
class TestProgressiveIO < Minitest::Test
|
|
8
9
|
|
|
@@ -192,12 +193,7 @@ class TestProgressiveIO < Minitest::Test
|
|
|
192
193
|
io.progress_block = lambda do | offset |
|
|
193
194
|
assert_equal 1, offset
|
|
194
195
|
end
|
|
195
|
-
|
|
196
|
-
if RUBY_VERSION < "1.9"
|
|
197
|
-
assert_equal 49, io.getc
|
|
198
|
-
else
|
|
199
|
-
assert_equal "1", io.getc
|
|
200
|
-
end
|
|
196
|
+
assert_equal "1", io.readchar
|
|
201
197
|
end
|
|
202
198
|
|
|
203
199
|
|
|
@@ -256,12 +252,13 @@ class TestProgressiveIO < Minitest::Test
|
|
|
256
252
|
assert_equal [[22]], m
|
|
257
253
|
end
|
|
258
254
|
|
|
259
|
-
def
|
|
260
|
-
io = e("Mary\nHad\nA little\nLamb")
|
|
261
|
-
io.progress_block = lambda
|
|
262
|
-
assert_equal 6, offset
|
|
263
|
-
end
|
|
255
|
+
def test_seek_is_not_a_read
|
|
256
|
+
io, m = e("Mary\nHad\nA little\nLamb"), []
|
|
257
|
+
io.progress_block = lambda { |offset| m << offset }
|
|
264
258
|
io.seek(6)
|
|
259
|
+
assert_empty m
|
|
260
|
+
io.read(2)
|
|
261
|
+
assert_equal [8], m
|
|
265
262
|
end
|
|
266
263
|
|
|
267
264
|
# def test_ungetc
|
|
@@ -276,6 +273,151 @@ class TestProgressiveIO < Minitest::Test
|
|
|
276
273
|
# assert_equal [[1], [0]], m
|
|
277
274
|
# end
|
|
278
275
|
|
|
276
|
+
def reported(io)
|
|
277
|
+
[].tap { |m| io.progress_block = lambda { |offset| m << offset } }
|
|
278
|
+
end
|
|
279
|
+
|
|
280
|
+
def test_readpartial
|
|
281
|
+
io = e("Mary\nHad\nA little\nLamb")
|
|
282
|
+
m = reported(io)
|
|
283
|
+
assert_equal "Mary", io.readpartial(4)
|
|
284
|
+
assert_equal [4], m
|
|
285
|
+
end
|
|
286
|
+
|
|
287
|
+
def test_read_nonblock
|
|
288
|
+
io = e("Mary\nHad\nA little\nLamb")
|
|
289
|
+
m = reported(io)
|
|
290
|
+
assert_equal "Mary", io.read_nonblock(4)
|
|
291
|
+
assert_equal "\nHad", io.read_nonblock(4, exception: false)
|
|
292
|
+
assert_equal [4, 8], m
|
|
293
|
+
end
|
|
294
|
+
|
|
295
|
+
def test_sysread
|
|
296
|
+
io = e("Mary\nHad\nA little\nLamb")
|
|
297
|
+
m = reported(io)
|
|
298
|
+
assert_equal "Mary", io.sysread(4)
|
|
299
|
+
assert_equal [4], m
|
|
300
|
+
end
|
|
301
|
+
|
|
302
|
+
def test_getbyte_and_readbyte
|
|
303
|
+
io = e("123")
|
|
304
|
+
m = reported(io)
|
|
305
|
+
assert_equal 49, io.getbyte
|
|
306
|
+
assert_equal 50, io.readbyte
|
|
307
|
+
assert_equal [1, 2], m
|
|
308
|
+
end
|
|
309
|
+
|
|
310
|
+
def test_each_char
|
|
311
|
+
io = e("abc")
|
|
312
|
+
m = reported(io)
|
|
313
|
+
assert_equal %w[a b c], io.each_char.to_a
|
|
314
|
+
assert_equal [1, 2, 3], m
|
|
315
|
+
end
|
|
316
|
+
|
|
317
|
+
def test_each_codepoint
|
|
318
|
+
io = e("ab")
|
|
319
|
+
m = reported(io)
|
|
320
|
+
assert_equal [97, 98], io.each_codepoint.to_a
|
|
321
|
+
assert_equal [1, 2], m
|
|
322
|
+
end
|
|
323
|
+
|
|
324
|
+
# IO.copy_stream reads anything that is not an IO with readpartial
|
|
325
|
+
def test_copy_stream_is_reported
|
|
326
|
+
io = e("Mary\nHad\nA little\nLamb")
|
|
327
|
+
m = reported(io)
|
|
328
|
+
out = StringIO.new
|
|
329
|
+
IO.copy_stream(io, out)
|
|
330
|
+
assert_equal "Mary\nHad\nA little\nLamb", out.string
|
|
331
|
+
assert_equal 22, m.last
|
|
332
|
+
end
|
|
333
|
+
|
|
334
|
+
# A real IO would be handed to IO.copy_stream through to_io and read past the wrapper
|
|
335
|
+
def test_copy_stream_from_a_file_is_reported
|
|
336
|
+
Tempfile.create("progressive") do |f|
|
|
337
|
+
f.write("x" * 100_000)
|
|
338
|
+
f.flush
|
|
339
|
+
io = ProgressiveIO.new(File.open(f.path))
|
|
340
|
+
m = reported(io)
|
|
341
|
+
IO.copy_stream(io, StringIO.new)
|
|
342
|
+
assert_equal 100_000, m.last
|
|
343
|
+
refute io.respond_to?(:to_io)
|
|
344
|
+
refute io.respond_to?(:to_path)
|
|
345
|
+
assert_raises(NoMethodError) { io.to_io }
|
|
346
|
+
assert_kind_of File, io.__getobj__
|
|
347
|
+
io.__getobj__.close
|
|
348
|
+
end
|
|
349
|
+
end
|
|
350
|
+
|
|
351
|
+
def test_keyword_arguments_are_passed_on
|
|
352
|
+
io = e("Mary\nHad\nA little\nLamb")
|
|
353
|
+
m = reported(io)
|
|
354
|
+
assert_equal "Mary", io.gets(chomp: true)
|
|
355
|
+
assert_equal "Had", io.readline(chomp: true)
|
|
356
|
+
assert_equal ["A little", "Lamb"], io.readlines(chomp: true)
|
|
357
|
+
assert_equal [5, 9, 22], m
|
|
358
|
+
|
|
359
|
+
assert_equal ["Mary", "Had"], e("Mary\nHad\n").each_line(chomp: true).to_a
|
|
360
|
+
lines = []
|
|
361
|
+
e("Mary\nHad\n").each(chomp: true) { |line| lines << line }
|
|
362
|
+
assert_equal ["Mary", "Had"], lines
|
|
363
|
+
end
|
|
364
|
+
|
|
365
|
+
# A pipe or a socket has no position to report, so what has been read so far stands in for it
|
|
366
|
+
def test_streams_without_a_position_report_the_bytes_read
|
|
367
|
+
r, w = IO.pipe
|
|
368
|
+
w.write("Mary\nHad\nA little\nLamb")
|
|
369
|
+
w.close
|
|
370
|
+
io = ProgressiveIO.new(r)
|
|
371
|
+
m = reported(io)
|
|
372
|
+
assert_equal "Mary\n", io.gets
|
|
373
|
+
assert_equal "Had", io.read(3)
|
|
374
|
+
assert_equal "\n".ord, io.getbyte
|
|
375
|
+
assert_equal ["A little\n", "Lamb"], io.each_line.to_a
|
|
376
|
+
assert_equal [5, 8, 9, 18, 22], m
|
|
377
|
+
ensure
|
|
378
|
+
r.close if r
|
|
379
|
+
end
|
|
380
|
+
|
|
381
|
+
def test_bytes_pushed_back_into_a_stream_are_not_counted_twice
|
|
382
|
+
r, w = IO.pipe
|
|
383
|
+
w.write("Mary")
|
|
384
|
+
w.close
|
|
385
|
+
io = ProgressiveIO.new(r)
|
|
386
|
+
m = reported(io)
|
|
387
|
+
io.getc
|
|
388
|
+
io.ungetc("M")
|
|
389
|
+
assert_equal "Mary", io.read
|
|
390
|
+
assert_equal [1, 4], m
|
|
391
|
+
ensure
|
|
392
|
+
r.close if r
|
|
393
|
+
end
|
|
394
|
+
|
|
395
|
+
def test_copy_stream_from_a_pipe
|
|
396
|
+
r, w = IO.pipe
|
|
397
|
+
# More than a pipe holds, so it has to be written while it is read
|
|
398
|
+
writer = Thread.new { w.write("x" * 200_000); w.close }
|
|
399
|
+
io = ProgressiveIO.new(r)
|
|
400
|
+
m = reported(io)
|
|
401
|
+
IO.copy_stream(io, StringIO.new)
|
|
402
|
+
writer.join
|
|
403
|
+
assert_equal 200_000, m.last
|
|
404
|
+
ensure
|
|
405
|
+
r.close if r
|
|
406
|
+
end
|
|
407
|
+
|
|
408
|
+
def test_counts_reads_made_before_a_block_was_set
|
|
409
|
+
r, w = IO.pipe
|
|
410
|
+
w.write("Mary\nHad\n")
|
|
411
|
+
w.close
|
|
412
|
+
io = ProgressiveIO.new(r)
|
|
413
|
+
io.gets
|
|
414
|
+
m = reported(io)
|
|
415
|
+
io.gets
|
|
416
|
+
assert_equal [9], m
|
|
417
|
+
ensure
|
|
418
|
+
r.close if r
|
|
419
|
+
end
|
|
420
|
+
|
|
279
421
|
def test_poseq
|
|
280
422
|
io = e("Mary\nHad\nA little\nLamb")
|
|
281
423
|
m = []
|
metadata
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: progressive_io
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 2.0
|
|
4
|
+
version: 2.1.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Julik Tarkhanov
|
|
8
8
|
bindir: bin
|
|
9
9
|
cert_chain: []
|
|
10
|
-
date: 2026-
|
|
10
|
+
date: 2026-10-30 00:00:00.000000000 Z
|
|
11
11
|
dependencies:
|
|
12
12
|
- !ruby/object:Gem::Dependency
|
|
13
13
|
name: rake
|
|
@@ -77,14 +77,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
|
77
77
|
requirements:
|
|
78
78
|
- - ">="
|
|
79
79
|
- !ruby/object:Gem::Version
|
|
80
|
-
version:
|
|
80
|
+
version: '3.2'
|
|
81
81
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
82
82
|
requirements:
|
|
83
83
|
- - ">="
|
|
84
84
|
- !ruby/object:Gem::Version
|
|
85
85
|
version: '0'
|
|
86
86
|
requirements: []
|
|
87
|
-
rubygems_version: 3.6.
|
|
87
|
+
rubygems_version: 3.6.2
|
|
88
88
|
specification_version: 4
|
|
89
89
|
summary: An IO wrapper that sends reports on the offset to a callback, on each read
|
|
90
90
|
operation
|