# For some inexplicable reason yaml MUST be required before # tempfile in order for ExtArrTest::test_LSHIFT to pass. # Otherwise it fails with 'TypeError: allocator undefined for Proc' require 'yaml' require 'tempfile' require 'external/enumerable' require 'external/io' module External # Base provides shared IO and Array-like methods used by ExternalArchive, # ExternalArray, and ExternalIndex. class Base class << self # Initializes an instance of self with File.open(path, mode) as an io. # As with File.open, the instance will be passed to the block and # closed when the block returns. If no block is given, open returns # the new instance. # # Nil may be provided as an fd, in which case a Tempfile will be # used (in which case mode gets ignored as Tempfiles always open # in 'r+' mode). def open(path=nil, mode="rb", *argv) begin io = path == nil ? nil : File.open(path, mode) base = new(io, *argv) rescue(Errno::ENOENT) io.close if io raise end if block_given? begin yield(base) ensure base.close end else base end end end include External::Enumerable include External::Chunkable # The underlying io for self. attr_reader :io # The default tempfile basename for Base instances # initialized without an io. TEMPFILE_BASENAME = "external_base" # Creates a new instance of self with the specified io. A # nil io causes initialization with a Tempfile; a string # io will be converted into a StringIO. def initialize(io=nil) self.io = case io when nil then Tempfile.new(TEMPFILE_BASENAME) when String then StringIO.new(io) else io end @enumerate_to_a = true end # True if io is closed. def closed? io.closed? end # Closes io. If a path is specified, io will be dumped to it. If # io is a File or Tempfile, the existing file is moved (not dumped) # to path. Raises an error if path already exists and overwrite is # not specified. def close(path=nil, overwrite=false) result = !io.closed? if path if File.exists?(path) && !overwrite raise ArgumentError, "already exists: #{path}" end case io when File, Tempfile io.close unless io.closed? FileUtils.move(io.path, path) else io.flush io.rewind File.open(path, "w") do |file| file << io.read(io.default_blksize) while !io.eof? end end end io.close unless io.closed? result end # Flushes the io and resets the io length. Returns self def flush io.flush io.reset_length self end # Returns a duplicate of self. This can be a slow operation # as it may involve copying the full contents of one large # file to another. def dup flush another.concat(self) end # Returns another instance of self. Must be # implemented in a subclass. def another raise NotImplementedError end ########################### # Array methods ########################### # Returns true if _self_ contains no elements def empty? length == 0 end def eql?(another) self == another end # Returns the first n entries (default 1) def first(n=nil) n.nil? ? self[0] : self[0,n] end # Alias for [] def slice(one, two = nil) self[one, two] end # Returns self. #-- # Warning -- errors show up when this doesn't return # an Array... however to return an array with to_ary # may mean converting a Base into an Array for # insertions... see/modify convert_to_ary def to_ary self end # def inspect "#<#{self.class}:#{object_id} #{ellipse_inspect(self)}>" end protected # Sets io and extends the input io with Io. def io=(io) # :nodoc: io.extend Io unless io.kind_of?(Io) @io = io end # converts obj to an int using the to_int # method, if the object responds to to_int def convert_to_int(obj) # :nodoc: obj.respond_to?(:to_int) ? obj.to_int : obj end # converts obj to an array using the to_ary # method, if the object responds to to_ary def convert_to_ary(obj) # :nodoc: obj == nil ? [] : obj.respond_to?(:to_ary) ? obj.to_ary : [obj] end # a more array-compliant version of Chunkable#split_range def split_range(range, total=length) # :nodoc: # split the range start = convert_to_int(range.begin) raise TypeError, "can't convert #{range.begin.class} into Integer" unless start.kind_of?(Integer) start += total if start < 0 finish = convert_to_int(range.end) raise TypeError, "can't convert #{range.end.class} into Integer" unless finish.kind_of?(Integer) finish += total if finish < 0 length = finish - start length -= 1 if range.exclude_end? [start, length] end # helper to inspect large arrays def ellipse_inspect(array) # :nodoc: if array.length > 10 "[#{collect_join(array[0,5])} ... #{collect_join(array[-5,5])}] (length = #{array.length})" else "[#{collect_join(array.to_a)}]" end end # another helper to inspect large arrays def collect_join(array) # :nodoc: array.collect do |obj| obj.inspect end.join(', ') end end end