Skip to content

Commit ce91198

Browse files
authored
Allow CSV.open with StringIO argument (#302)
Fix #300
1 parent bb93c28 commit ce91198

File tree

2 files changed

+74
-18
lines changed

2 files changed

+74
-18
lines changed

lib/csv.rb

+56-18
Original file line numberDiff line numberDiff line change
@@ -1508,10 +1508,8 @@ def generate_lines(rows, **options)
15081508

15091509
#
15101510
# :call-seq:
1511-
# open(file_path, mode = "rb", **options ) -> new_csv
1512-
# open(io, mode = "rb", **options ) -> new_csv
1513-
# open(file_path, mode = "rb", **options ) { |csv| ... } -> object
1514-
# open(io, mode = "rb", **options ) { |csv| ... } -> object
1511+
# open(path_or_io, mode = "rb", **options ) -> new_csv
1512+
# open(path_or_io, mode = "rb", **options ) { |csv| ... } -> object
15151513
#
15161514
# possible options elements:
15171515
# keyword form:
@@ -1520,7 +1518,7 @@ def generate_lines(rows, **options)
15201518
# :undef => :replace # replace undefined conversion
15211519
# :replace => string # replacement string ("?" or "\uFFFD" if not specified)
15221520
#
1523-
# * Argument +path+, if given, must be the path to a file.
1521+
# * Argument +path_or_io+, must be a file path or an \IO stream.
15241522
# :include: ../doc/csv/arguments/io.rdoc
15251523
# * Argument +mode+, if given, must be a \File mode.
15261524
# See {Access Modes}[https://docs.ruby-lang.org/en/master/File.html#class-File-label-Access+Modes].
@@ -1544,6 +1542,9 @@ def generate_lines(rows, **options)
15441542
# path = 't.csv'
15451543
# File.write(path, string)
15461544
#
1545+
# string_io = StringIO.new
1546+
# string_io << "foo,0\nbar,1\nbaz,2\n"
1547+
#
15471548
# ---
15481549
#
15491550
# With no block given, returns a new \CSV object.
@@ -1556,6 +1557,9 @@ def generate_lines(rows, **options)
15561557
# csv = CSV.open(File.open(path))
15571558
# csv # => #<CSV io_type:File io_path:"t.csv" encoding:UTF-8 lineno:0 col_sep:"," row_sep:"\n" quote_char:"\"">
15581559
#
1560+
# Create a \CSV object using a \StringIO:
1561+
# csv = CSV.open(string_io)
1562+
# csv # => #<CSV io_type:StringIO encoding:UTF-8 lineno:0 col_sep:"," row_sep:"\n" quote_char:"\"">
15591563
# ---
15601564
#
15611565
# With a block given, calls the block with the created \CSV object;
@@ -1573,16 +1577,24 @@ def generate_lines(rows, **options)
15731577
# Output:
15741578
# #<CSV io_type:File io_path:"t.csv" encoding:UTF-8 lineno:0 col_sep:"," row_sep:"\n" quote_char:"\"">
15751579
#
1580+
# Using a \StringIO:
1581+
# csv = CSV.open(string_io) {|csv| p csv}
1582+
# csv # => #<CSV io_type:StringIO encoding:UTF-8 lineno:0 col_sep:"," row_sep:"\n" quote_char:"\"">
1583+
# Output:
1584+
# #<CSV io_type:StringIO encoding:UTF-8 lineno:0 col_sep:"," row_sep:"\n" quote_char:"\"">
15761585
# ---
15771586
#
15781587
# Raises an exception if the argument is not a \String object or \IO object:
15791588
# # Raises TypeError (no implicit conversion of Symbol into String)
15801589
# CSV.open(:foo)
1581-
def open(filename, mode="r", **options)
1590+
def open(filename_or_io, mode="r", **options)
15821591
# wrap a File opened with the remaining +args+ with no newline
15831592
# decorator
15841593
file_opts = {}
1585-
may_enable_bom_deletection_automatically(mode, options, file_opts)
1594+
may_enable_bom_detection_automatically(filename_or_io,
1595+
mode,
1596+
options,
1597+
file_opts)
15861598
file_opts.merge!(options)
15871599
unless file_opts.key?(:newline)
15881600
file_opts[:universal_newline] ||= false
@@ -1592,14 +1604,19 @@ def open(filename, mode="r", **options)
15921604
options.delete(:replace)
15931605
options.delete_if {|k, _| /newline\z/.match?(k)}
15941606

1595-
begin
1596-
f = File.open(filename, mode, **file_opts)
1597-
rescue ArgumentError => e
1598-
raise unless /needs binmode/.match?(e.message) and mode == "r"
1599-
mode = "rb"
1600-
file_opts = {encoding: Encoding.default_external}.merge(file_opts)
1601-
retry
1607+
if filename_or_io.is_a?(StringIO)
1608+
f = create_stringio(filename_or_io.string, mode, **file_opts)
1609+
else
1610+
begin
1611+
f = File.open(filename_or_io, mode, **file_opts)
1612+
rescue ArgumentError => e
1613+
raise unless /needs binmode/.match?(e.message) and mode == "r"
1614+
mode = "rb"
1615+
file_opts = {encoding: Encoding.default_external}.merge(file_opts)
1616+
retry
1617+
end
16021618
end
1619+
16031620
begin
16041621
csv = new(f, **options)
16051622
rescue Exception
@@ -1886,16 +1903,37 @@ def table(path, **options)
18861903
private_constant :ON_WINDOWS
18871904

18881905
private
1889-
def may_enable_bom_deletection_automatically(mode, options, file_opts)
1890-
# "bom|utf-8" may be buggy on Windows:
1891-
# https://bugs.ruby-lang.org/issues/20526
1892-
return if ON_WINDOWS
1906+
def may_enable_bom_detection_automatically(filename_or_io,
1907+
mode,
1908+
options,
1909+
file_opts)
1910+
if filename_or_io.is_a?(StringIO)
1911+
# Support to StringIO was dropped for Ruby 2.6 and earlier without BOM support:
1912+
# https://github.com/ruby/stringio/pull/47
1913+
return if RUBY_VERSION < "2.7"
1914+
else
1915+
# "bom|utf-8" may be buggy on Windows:
1916+
# https://bugs.ruby-lang.org/issues/20526
1917+
return if ON_WINDOWS
1918+
end
18931919
return unless Encoding.default_external == Encoding::UTF_8
18941920
return if options.key?(:encoding)
18951921
return if options.key?(:external_encoding)
18961922
return if mode.include?(":")
18971923
file_opts[:encoding] = "bom|utf-8"
18981924
end
1925+
1926+
if RUBY_VERSION < "2.7"
1927+
def create_stringio(str, mode, opts)
1928+
opts.delete_if {|k, _| k == :universal_newline or DEFAULT_OPTIONS.key?(k)}
1929+
raise ArgumentError, "Unsupported options parsing StringIO: #{opts.keys}" unless opts.empty?
1930+
StringIO.new(str, mode)
1931+
end
1932+
else
1933+
def create_stringio(str, mode, opts)
1934+
StringIO.new(str, mode, **opts)
1935+
end
1936+
end
18991937
end
19001938

19011939
# :call-seq:

test/csv/interface/test_read.rb

+18
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,24 @@ def test_foreach
3232
assert_equal(@rows, rows)
3333
end
3434

35+
def test_foreach_stringio
36+
string_io = StringIO.new(@data)
37+
rows = CSV.foreach(string_io, col_sep: "\t", row_sep: "\r\n").to_a
38+
assert_equal(@rows, rows)
39+
end
40+
41+
def test_foreach_stringio_with_bom
42+
if RUBY_VERSION < "2.7"
43+
# Support to StringIO was dropped for Ruby 2.6 and earlier without BOM support:
44+
# https://github.com/ruby/stringio/pull/47
45+
omit("StringIO's BOM support isn't available with Ruby < 2.7")
46+
end
47+
48+
string_io = StringIO.new("\ufeff#{@data}") # U+FEFF ZERO WIDTH NO-BREAK SPACE
49+
rows = CSV.foreach(string_io, col_sep: "\t", row_sep: "\r\n").to_a
50+
assert_equal(@rows, rows)
51+
end
52+
3553
if respond_to?(:ractor)
3654
ractor
3755
def test_foreach_in_ractor

0 commit comments

Comments
 (0)