@@ -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:
0 commit comments