Skip to content

Commit

Permalink
Add support for Ractor (#218)
Browse files Browse the repository at this point in the history
  • Loading branch information
rm155 committed Oct 11, 2021
1 parent 29cef9e commit a802690
Show file tree
Hide file tree
Showing 5 changed files with 94 additions and 6 deletions.
2 changes: 1 addition & 1 deletion csv.gemspec
Expand Up @@ -60,5 +60,5 @@ Gem::Specification.new do |spec|
spec.add_development_dependency "bundler"
spec.add_development_dependency "rake"
spec.add_development_dependency "benchmark_driver"
spec.add_development_dependency "test-unit", ">= 3.4.3"
spec.add_development_dependency "test-unit", ">= 3.4.8"
end
21 changes: 21 additions & 0 deletions lib/csv.rb
Expand Up @@ -547,6 +547,14 @@
#
# There is no such storage structure for write headers.
#
# In order for the parsing methods to access stored converters in non-main-Ractors, the
# storage structure must be made shareable first.
# Therefore, <tt>Ractor.make_shareable(CSV::Converters)</tt> and
# <tt>Ractor.make_shareable(CSV::HeaderConverters)</tt> must be called before the creation
# of Ractors that use the converters stored in these structures. (Since making the storage
# structures shareable involves freezing them, any custom converters that are to be used
# must be added first.)
#
# ===== Converter Lists
#
# A _converter_ _list_ is an \Array that may include any assortment of:
Expand Down Expand Up @@ -919,6 +927,7 @@ def initialize(message, line_number)
gsub(/\s+/, "_").to_sym
}
}

# Default values for method options.
DEFAULT_OPTIONS = {
# For both parsing and generating.
Expand Down Expand Up @@ -957,6 +966,8 @@ class << self
# Creates or retrieves cached \CSV objects.
# For arguments and options, see CSV.new.
#
# This API is not Ractor-safe.
#
# ---
#
# With no block given, returns a \CSV object.
Expand Down Expand Up @@ -1992,6 +2003,10 @@ def skip_lines
# csv.converters # => [:integer]
# csv.convert(proc {|x| x.to_s })
# csv.converters
#
# Notes that you need to call
# +Ractor.make_shareable(CSV::Converters)+ on the main Ractor to use
# this method.
def converters
parser_fields_converter.map do |converter|
name = Converters.rassoc(converter)
Expand Down Expand Up @@ -2054,6 +2069,10 @@ def write_headers?
# Returns an \Array containing header converters; used for parsing;
# see {Header Converters}[#class-CSV-label-Header+Converters]:
# CSV.new('').header_converters # => []
#
# Notes that you need to call
# +Ractor.make_shareable(CSV::HeaderConverters)+ on the main Ractor
# to use this method.
def header_converters
header_fields_converter.map do |converter|
name = HeaderConverters.rassoc(converter)
Expand Down Expand Up @@ -2774,6 +2793,8 @@ def writer_options
# io = StringIO.new
# CSV(io, col_sep: ";") { |csv| csv << ["a", "b", "c"] }
#
# This API is not Ractor-safe.
#
def CSV(*args, **options, &block)
CSV.instance(*args, **options, &block)
end
Expand Down
11 changes: 6 additions & 5 deletions lib/csv/parser.rb
Expand Up @@ -480,9 +480,9 @@ def prepare_strip
begin
StringScanner.new("x").scan("x")
rescue TypeError
@@string_scanner_scan_accept_string = false
STRING_SCANNER_SCAN_ACCEPT_STRING = false
else
@@string_scanner_scan_accept_string = true
STRING_SCANNER_SCAN_ACCEPT_STRING = true
end

def prepare_separators
Expand All @@ -506,7 +506,7 @@ def prepare_separators
@first_column_separators = Regexp.new(@escaped_first_column_separator +
"+".encode(@encoding))
else
if @@string_scanner_scan_accept_string
if STRING_SCANNER_SCAN_ACCEPT_STRING
@column_end = @column_separator
else
@column_end = Regexp.new(@escaped_column_separator)
Expand Down Expand Up @@ -725,6 +725,8 @@ def eof?
end
end

SCANNER_TEST_CHUNK_SIZE =
Integer((ENV["CSV_PARSER_SCANNER_TEST_CHUNK_SIZE"] || "1"), 10)
def build_scanner
inputs = @samples.collect do |sample|
UnoptimizedStringIO.new(sample)
Expand All @@ -734,10 +736,9 @@ def build_scanner
else
inputs << @input
end
chunk_size = ENV["CSV_PARSER_SCANNER_TEST_CHUNK_SIZE"] || "1"
InputsScanner.new(inputs,
@encoding,
chunk_size: Integer(chunk_size, 10))
chunk_size: SCANNER_TEST_CHUNK_SIZE)
end
else
def build_scanner
Expand Down
32 changes: 32 additions & 0 deletions test/csv/interface/test_read.rb
Expand Up @@ -32,6 +32,24 @@ def test_foreach
assert_equal(@rows, rows)
end

if respond_to?(:ractor)
ractor
def test_foreach_in_ractor
ractor = Ractor.new(@input.path) do |path|
rows = []
CSV.foreach(path, col_sep: "\t", row_sep: "\r\n").each do |row|
rows << row
end
rows
end
rows = [
["1", "2", "3"],
["4", "5"],
]
assert_equal(rows, ractor.take)
end
end

def test_foreach_mode
rows = []
CSV.foreach(@input.path, "r", col_sep: "\t", row_sep: "\r\n").each do |row|
Expand Down Expand Up @@ -250,6 +268,20 @@ def test_read
CSV.read(@input.path, col_sep: "\t", row_sep: "\r\n"))
end

if respond_to?(:ractor)
ractor
def test_read_in_ractor
ractor = Ractor.new(@input.path) do |path|
CSV.read(path, col_sep: "\t", row_sep: "\r\n")
end
rows = [
["1", "2", "3"],
["4", "5"],
]
assert_equal(rows, ractor.take)
end
end

def test_readlines
assert_equal(@rows,
CSV.readlines(@input.path, col_sep: "\t", row_sep: "\r\n"))
Expand Down
34 changes: 34 additions & 0 deletions test/csv/interface/test_write.rb
Expand Up @@ -25,6 +25,21 @@ def test_generate_default
CSV
end

if respond_to?(:ractor)
ractor
def test_generate_default_in_ractor
ractor = Ractor.new do
CSV.generate do |csv|
csv << [1, 2, 3] << [4, nil, 5]
end
end
assert_equal(<<-CSV, ractor.take)
1,2,3
4,,5
CSV
end
end

def test_generate_append
csv_text = <<-CSV
1,2,3
Expand Down Expand Up @@ -101,6 +116,25 @@ def test_append_row
CSV
end


if respond_to?(:ractor)
ractor
def test_append_row_in_ractor
ractor = Ractor.new(@output.path) do |path|
CSV.open(path, "wb") do |csv|
csv <<
CSV::Row.new([], ["1", "2", "3"]) <<
CSV::Row.new([], ["a", "b", "c"])
end
end
ractor.take
assert_equal(<<-CSV, File.read(@output.path, mode: "rb"))
1,2,3
a,b,c
CSV
end
end

def test_append_hash
CSV.open(@output.path, "wb", headers: true) do |csv|
csv << [:a, :b, :c]
Expand Down

0 comments on commit a802690

Please sign in to comment.