youplot 0.4.6 → 0.5.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.
@@ -1,9 +1,11 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'tempfile'
4
+ require 'stringio'
5
+
3
6
  require_relative 'dsv'
4
7
  require_relative 'parser'
5
8
 
6
- # FIXME
7
9
  require_relative 'backends/unicode_plot'
8
10
 
9
11
  module YouPlot
@@ -42,23 +44,7 @@ module YouPlot
42
44
 
43
45
  # progressive mode
44
46
  if options[:progressive]
45
- stop = false
46
- Signal.trap(:INT) { stop = true }
47
-
48
- # make cursor invisible
49
- options[:output].print "\e[?25l"
50
-
51
- # mainloop
52
- while (input = Kernel.gets)
53
- n = main_progressive(input)
54
- break if stop
55
-
56
- options[:output].print "\e[#{n}F"
57
- end
58
-
59
- options[:output].print "\e[0J"
60
- # make cursor visible
61
- options[:output].print "\e[?25h"
47
+ run_progressive
62
48
 
63
49
  # normal mode
64
50
  else
@@ -77,6 +63,47 @@ module YouPlot
77
63
 
78
64
  private
79
65
 
66
+ def run_progressive
67
+ out = progressive_output
68
+ stop = false
69
+ last_rendered_lines = 0
70
+ Signal.trap(:INT) { stop = true }
71
+
72
+ # make cursor invisible
73
+ out.print "\e[?25l"
74
+
75
+ # mainloop
76
+ begin
77
+ while (input = Kernel.gets)
78
+ n = main_progressive(input)
79
+ # Track the latest plot height so cleanup can move below the plot.
80
+ last_rendered_lines = n if n && n > 0
81
+ break if stop
82
+
83
+ out.print "\e[#{n}F" if n && n > 0
84
+ end
85
+ ensure
86
+ sanitize_progressive_output(out, last_rendered_lines)
87
+ end
88
+ end
89
+
90
+ def progressive_output
91
+ out = options[:output]
92
+ raise 'In progressive mode, output to a file is not possible.' if out.is_a?(String)
93
+ return out if out.respond_to?(:print) && out.respond_to?(:flush)
94
+
95
+ raise 'In progressive mode, output to a file is not possible.'
96
+ end
97
+
98
+ def sanitize_progressive_output(out = progressive_output, last_rendered_lines = 0)
99
+ # Move below the last rendered plot (CSI n E).
100
+ out.print "\e[#{last_rendered_lines}E" if last_rendered_lines > 0
101
+ # Clear from cursor to end of screen (CSI 0 J).
102
+ out.print "\e[0J"
103
+ # Show cursor again (CSI ?25 h).
104
+ out.print "\e[?25h"
105
+ end
106
+
80
107
  def main(input)
81
108
  # Outputs input data to a file or stdout.
82
109
  output_data(input)
@@ -110,31 +137,98 @@ module YouPlot
110
137
  def main_progressive(input)
111
138
  output_data(input)
112
139
 
113
- # FIXME
114
- # Worked around the problem of not being able to draw
115
- # plots when there is only one header line.
116
- if @raw_data.nil?
117
- @raw_data = String.new
118
- if options[:headers]
119
- @raw_data << input
120
- return
121
- end
122
- end
123
- @raw_data << input
140
+ row = parse_progressive_row(input)
141
+ return 0 if row.nil?
124
142
 
125
- # FIXME
126
- @data = parse_dsv(@raw_data)
143
+ @data = progressive_update_data(row)
144
+ return 0 if @data.nil?
127
145
 
128
146
  plot = create_plot
129
147
  output_plot_progressive(plot)
130
148
  end
131
149
 
150
+ def parse_progressive_row(input)
151
+ line = normalize_input_encoding!(input)
152
+
153
+ begin
154
+ row = CSV.parse_line(line, col_sep: options[:delimiter])
155
+ rescue CSV::MalformedCSVError => e
156
+ warn 'Failed to parse the text. '
157
+ warn 'Please try to set the correct character encoding with --encoding option.'
158
+ warn e.backtrace.grep(/youplot/).first
159
+ exit 1
160
+ rescue ArgumentError => e
161
+ warn 'Failed to parse the text. '
162
+ warn e.backtrace.grep(/youplot/).first
163
+ exit 1
164
+ end
165
+
166
+ return nil if row.nil? || row.empty? || row.all?(&:nil?)
167
+
168
+ row
169
+ end
170
+
171
+ def progressive_update_data(row)
172
+ init_progressive_state
173
+
174
+ return nil if consume_progressive_header?(row)
175
+
176
+ append_progressive_row(row)
177
+ progressive_data
178
+ end
179
+
180
+ def init_progressive_state
181
+ return if @progressive_initialized
182
+
183
+ @progressive_initialized = true
184
+ @progressive_headers = options[:headers] ? [] : nil
185
+ @progressive_series = []
186
+ @progressive_header_consumed = false
187
+ @progressive_row_count = 0
188
+ end
189
+
190
+ def consume_progressive_header?(row)
191
+ return false unless options[:headers]
192
+ return false if options[:transpose]
193
+ return false if @progressive_header_consumed
194
+
195
+ @progressive_headers = row
196
+ @progressive_header_consumed = true
197
+ true
198
+ end
199
+
200
+ def append_progressive_row(row)
201
+ if options[:headers] && options[:transpose]
202
+ @progressive_headers << row[0]
203
+ @progressive_series << row[1..-1]
204
+ elsif options[:transpose]
205
+ @progressive_series << row
206
+ else
207
+ append_progressive_columns(row)
208
+ end
209
+ end
210
+
211
+ def progressive_data
212
+ DSV.build_data(@progressive_headers, @progressive_series)
213
+ end
214
+
215
+ def append_progressive_columns(row)
216
+ if row.size > @progressive_series.size
217
+ (@progressive_series.size...row.size).each do |i|
218
+ @progressive_series[i] = Array.new(@progressive_row_count, nil)
219
+ end
220
+ end
221
+
222
+ 0.upto(@progressive_series.size - 1) do |i|
223
+ @progressive_series[i] << row[i]
224
+ end
225
+
226
+ @progressive_row_count += 1
227
+ end
228
+
132
229
  def parse_dsv(input)
133
230
  # If encoding is specified, convert to UTF-8
134
- if options[:encoding]
135
- input.force_encoding(options[:encoding])
136
- .encode!('utf-8')
137
- end
231
+ normalize_input_encoding!(input)
138
232
 
139
233
  begin
140
234
  data = DSV.parse(input, options[:delimiter], options[:headers], options[:transpose])
@@ -152,6 +246,13 @@ module YouPlot
152
246
  data
153
247
  end
154
248
 
249
+ def normalize_input_encoding!(input)
250
+ return input unless options[:encoding]
251
+
252
+ input.force_encoding(options[:encoding])
253
+ .encode!('utf-8')
254
+ end
255
+
155
256
  def create_plot
156
257
  case command
157
258
  when :bar, :barplot
@@ -179,50 +280,67 @@ module YouPlot
179
280
 
180
281
  def output_data(input)
181
282
  # Pass the input to subsequent pipelines
182
- case options[:pass]
183
- when IO, StringIO
184
- options[:pass].print(input)
185
- else
186
- if options[:pass]
187
- File.open(options[:pass], 'w') do |f|
188
- f.print(input)
189
- end
283
+ out = options[:pass]
284
+ # Handle Tempfile first to keep tests and behavior consistent.
285
+ # Ruby 2.7 Tempfile is Delegator-based and does not match IO/File checks.
286
+ # Then handle path strings and IO-like objects.
287
+ case out
288
+ when Tempfile
289
+ # Keep file descriptor state consistent with the Tempfile object.
290
+ out.truncate(0) # clear existing content
291
+ out.rewind # move pointer to the beginning
292
+ out.print(input) # write new content
293
+ out.flush # flush buffered writes before immediate read
294
+ out.rewind # move pointer back to the beginning for out.read
295
+ when String
296
+ File.open(out, 'w') do |f|
297
+ f.print(input)
190
298
  end
299
+ else
300
+ out.print(input) if out.respond_to?(:print)
191
301
  end
192
302
  end
193
303
 
194
304
  def output_plot(plot)
195
- case options[:output]
196
- when IO, StringIO
197
- plot.render(options[:output])
198
- when String, Tempfile
199
- File.open(options[:output], 'w') do |f|
305
+ out = options[:output]
306
+ # Handle Tempfile first to keep tests and behavior consistent.
307
+ # Ruby 2.7 Tempfile is Delegator-based and does not match IO/File checks.
308
+ # Then handle path strings and IO-like objects.
309
+ case out
310
+ when Tempfile
311
+ # Keep file descriptor state consistent with the Tempfile object.
312
+ out.truncate(0) # clear existing content
313
+ out.rewind # move pointer to the beginning
314
+ plot.render(out) # write new content
315
+ out.flush # flush buffered writes before immediate read
316
+ out.rewind # move pointer back to the beginning for out.read
317
+ when String
318
+ File.open(out, 'w') do |f|
200
319
  plot.render(f)
201
320
  end
321
+ else
322
+ plot.render(out) if out.respond_to?(:write)
202
323
  end
203
324
  end
204
325
 
205
326
  def output_plot_progressive(plot)
206
- case options[:output]
207
- when IO, StringIO
208
- # RefactorMe
209
- out = StringIO.new(String.new)
210
- def out.tty?
211
- true
212
- end
213
- plot.render(out)
214
- lines = out.string.lines
215
- lines.each do |line|
216
- options[:output].print line.chomp
217
- options[:output].print "\e[0K"
218
- options[:output].puts
219
- end
220
- options[:output].print "\e[0J"
221
- options[:output].flush
222
- out.string.lines.size
223
- else
224
- raise 'In progressive mode, output to a file is not possible.'
327
+ target = progressive_output
328
+
329
+ # RefactorMe
330
+ out = StringIO.new(String.new)
331
+ def out.tty?
332
+ true
333
+ end
334
+ plot.render(out)
335
+ lines = out.string.lines
336
+ lines.each do |line|
337
+ target.print line.chomp
338
+ target.print "\e[0K"
339
+ target.puts
225
340
  end
341
+ target.print "\e[0J"
342
+ target.flush
343
+ lines.size
226
344
  end
227
345
  end
228
346
  end
data/lib/youplot/dsv.rb CHANGED
@@ -23,9 +23,24 @@ module YouPlot
23
23
  # get series
24
24
  series = get_series(arr, headers, transpose)
25
25
 
26
+ build_data(headers, series)
27
+ end
28
+
29
+ # Transpose different sized ruby arrays
30
+ # https://stackoverflow.com/q/26016632
31
+ def transpose2(arr)
32
+ Array.new(arr.map(&:length).max) { |i| arr.map { |e| e[i] } }
33
+ end
34
+
35
+ def build_data(headers, series)
26
36
  # Return if No header
27
37
  return Data.new(headers, series) if headers.nil?
28
38
 
39
+ h_size, s_size = validate_headers(headers, series)
40
+ Data.new(headers, series) if h_size == s_size
41
+ end
42
+
43
+ def validate_headers(headers, series)
29
44
  # Warn if header contains nil
30
45
  warn "\e[35mHeaders contains nil in it.\e[0m" if headers.include?(nil)
31
46
 
@@ -39,19 +54,12 @@ module YouPlot
39
54
  if h_size > s_size
40
55
  warn "\e[35mThe number of headers is greater than the number of series.\e[0m"
41
56
  exit 1 if YouPlot.run_as_executable?
42
-
43
57
  elsif h_size < s_size
44
58
  warn "\e[35mThe number of headers is less than the number of series.\e[0m"
45
59
  exit 1 if YouPlot.run_as_executable?
46
60
  end
47
61
 
48
- Data.new(headers, series) if h_size == s_size
49
- end
50
-
51
- # Transpose different sized ruby arrays
52
- # https://stackoverflow.com/q/26016632
53
- def transpose2(arr)
54
- Array.new(arr.map(&:length).max) { |i| arr.map { |e| e[i] } }
62
+ [h_size, s_size]
55
63
  end
56
64
 
57
65
  def get_headers(arr, headers, transpose)
@@ -15,4 +15,24 @@ module YouPlot
15
15
  :color_names, # color
16
16
  :debug
17
17
  )
18
+
19
+ # Default values for options.
20
+ # These are applied in Parser#resolve_options.
21
+ # Based on the following priority:
22
+ # 1. CLI options (highest priority)
23
+ # 2. Config file options
24
+ # 3. Default values (lowest priority) specified here.
25
+ Options::DEFAULTS = {
26
+ delimiter: "\t",
27
+ transpose: false,
28
+ headers: nil,
29
+ pass: false,
30
+ output: nil, # resolved to $stderr at parse time (late binding)
31
+ fmt: 'xyy',
32
+ progressive: false,
33
+ encoding: nil,
34
+ reverse: false,
35
+ color_names: false,
36
+ debug: false
37
+ }.freeze
18
38
  end
@@ -14,28 +14,14 @@ module YouPlot
14
14
 
15
15
  def initialize
16
16
  @command = nil
17
-
18
- @options = Options.new(
19
- "\t", # elimiter:
20
- false, # transpose:
21
- nil, # headers:
22
- false, # pass:
23
- $stderr, # output:
24
- 'xyy', # fmt:
25
- false, # progressive:
26
- nil, # encoding:
27
- false, # color_names:
28
- false # debug:
29
- )
30
-
31
- @params = Parameters.new
17
+ @options = Options.new
18
+ @params = Parameters.new
32
19
  end
33
20
 
34
21
  def apply_config_file
35
22
  return if !config_file && find_config_file.nil?
36
23
 
37
24
  read_config_file
38
- configure
39
25
  end
40
26
 
41
27
  def config_file_candidate_paths
@@ -70,20 +56,45 @@ module YouPlot
70
56
  @config = YAML.load_file(config_file)
71
57
  end
72
58
 
73
- def configure
74
- option_members = @options.members
75
- param_members = @params.members
76
- # It would be more useful to be able to configure by plot type
77
- config.each do |k, v|
78
- k = k.to_sym
79
- if option_members.include?(k)
80
- @options[k] ||= v
81
- elsif param_members.include?(k)
82
- @params[k] ||= v
83
- else
84
- raise Error, "Unknown option/param in config file: #{k}"
59
+ # Resolve options by applying the following priority:
60
+ # 1. CLI options -- cli_val = @options[k]
61
+ # 2. Config file -- cfg_val = @config[k.to_s]
62
+ # 3. DEFAULTS -- def_val from Options::DEFAULTS
63
+ def resolve_options
64
+ # Validate config keys up front.
65
+ if @config
66
+ known = (@options.members + @params.members).map(&:to_s)
67
+ @config.each_key do |k|
68
+ raise Error, "Unknown option/param in config file: #{k}" unless known.include?(k)
85
69
  end
86
70
  end
71
+
72
+ Options::DEFAULTS.each do |k, def_val|
73
+ cfg_val = @config && @config[k.to_s]
74
+ cli_val = @options[k]
75
+ @options[k] = if !cli_val.nil? # can be false
76
+ cli_val
77
+ elsif !cfg_val.nil? # can be false
78
+ cfg_val
79
+ else
80
+ def_val
81
+ end
82
+ end
83
+
84
+ # $stderr is evaluated here, not in DEFAULTS.
85
+ # DEFAULTS is a constant, so values in it are fixed at class load time.
86
+ # Tests redirect $stderr = tempfile after load, so placing $stderr in DEFAULTS
87
+ # would capture the original stderr and ignore the test redirect.
88
+ @options[:output] = $stderr if @options[:output].nil?
89
+ @options[:output] = $stdout if @options[:output] == '-'
90
+ @options[:pass] = $stdout if @options[:pass] == '-'
91
+
92
+ @params.members.each do |k|
93
+ cfg_val = @config && @config[k.to_s]
94
+ cli_val = @params[k]
95
+ # no def_val for params
96
+ @params[k] = cfg_val if cli_val.nil? && !cfg_val.nil?
97
+ end
87
98
  end
88
99
 
89
100
  def create_base_parser
@@ -96,11 +107,11 @@ module YouPlot
96
107
  parser.on('Common options:')
97
108
  parser.on('-O', '--pass [FILE]', 'file to output input data to [stdout]',
98
109
  'for inserting YouPlot in the middle of Unix pipes') do |v|
99
- options[:pass] = v || $stdout
110
+ options[:pass] = v.nil? || v == '-' ? $stdout : v
100
111
  end
101
112
  parser.on('-o', '--output [FILE]', 'file to output plots to [stdout]',
102
113
  'If no option is specified, plot will print to stderr') do |v|
103
- options[:output] = v || $stdout
114
+ options[:output] = v.nil? || v == '-' ? $stdout : v
104
115
  end
105
116
  parser.on('-d', '--delimiter DELIM', String, 'use DELIM instead of [TAB] for field delimiter') do |v|
106
117
  options[:delimiter] = v
@@ -217,8 +228,10 @@ module YouPlot
217
228
  end
218
229
 
219
230
  def show_config_info
220
- if ENV['MYYOUPLOTRC']
221
- puts "config file : #{ENV['MYYOUPLOTRC']}"
231
+ ensure_config_loaded
232
+
233
+ if @config_file
234
+ puts "config file : #{@config_file}"
222
235
  puts config.inspect
223
236
  else
224
237
  puts <<~EOS
@@ -234,6 +247,15 @@ module YouPlot
234
247
  exit if YouPlot.run_as_executable?
235
248
  end
236
249
 
250
+ def ensure_config_loaded
251
+ apply_config_file unless config
252
+ rescue StandardError => e
253
+ raise unless YouPlot.run_as_executable?
254
+
255
+ warn "YouPlot: #{e.message}"
256
+ exit 1
257
+ end
258
+
237
259
  def sub_parser_add_symbol
238
260
  sub_parser.on_head('--symbol STR', String, 'character to be used to plot the bars') do |v|
239
261
  params.symbol = v
@@ -383,6 +405,9 @@ module YouPlot
383
405
  end
384
406
 
385
407
  def parse_options(argv = ARGV)
408
+ # keep original ARGV intact.
409
+ argv = argv.equal?(ARGV) ? argv : argv.dup
410
+
386
411
  begin
387
412
  create_main_parser.order!(argv)
388
413
  rescue OptionParser::ParseError => e
@@ -399,12 +424,15 @@ module YouPlot
399
424
  exit 1 if YouPlot.run_as_executable?
400
425
  end
401
426
 
427
+ # Read config after CLI parsing, then resolve: defaults < config < CLI.
402
428
  begin
403
429
  apply_config_file
404
430
  rescue StandardError => e
405
431
  warn "YouPlot: #{e.message}"
406
432
  exit 1 if YouPlot.run_as_executable?
407
433
  end
434
+
435
+ resolve_options
408
436
  end
409
437
  end
410
438
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module YouPlot
4
- VERSION = '0.4.6'
4
+ VERSION = '0.5.0'
5
5
  end