Source

rubypython / lib / rubypython / conversion.rb

Full commit
require 'rubypython/python'
require 'rubypython/macros'

# Acts as a namespace for methods to bidirectionally convert between native
# Ruby types and native \Python types. Unsupported conversions raise
# UnsupportedConversion.
#
# The methods in this module should be considered internal implementation to
# RubyPython as they all return FFI pointers to \Python objects.
module RubyPython::Conversion
  # Raised when RubyPython does not know how to convert an object from
  # \Python to Ruby or vice versa.
  class UnsupportedConversion < Exception; end

  # Convert a Ruby string to a \Python string. Returns an FFI::Pointer to
  # a PyStringObject.
  def self.rtopString(rString)
    size = rString.respond_to?(:bytesize) ? rString.bytesize : rString.size
    RubyPython::Python.PyString_FromStringAndSize(rString, size)
  end

  # Convert a Ruby Array to \Python List. Returns an FFI::Pointer to
  # a PyListObject.
  def self.rtopArrayToList(rArray)
    size = rArray.length
    pList = RubyPython::Python.PyList_New size
    rArray.each_with_index do |el, i|
      RubyPython::Python.PyList_SetItem pList, i, rtopObject(el)
    end
    pList
  end

  # Convert a Ruby Array (including the subclass RubyPython::Tuple) to
  # \Python \tuple. Returns an FFI::Pointer to a PyTupleObject.
  def self.rtopArrayToTuple(rArray)
    pList = rtopArrayToList(rArray)
    pTuple = RubyPython::Python.PySequence_Tuple(pList)
    RubyPython::Python.Py_DecRef(pList)
    pTuple
  end

  # Convert a Ruby Hash to a \Python Dict. Returns an FFI::Pointer to a
  # PyDictObject.
  def self.rtopHash(rHash)
    pDict = RubyPython::Python.PyDict_New
    rHash.each do |k,v|
      RubyPython::Python.PyDict_SetItem pDict, rtopObject(k, key = true),
        rtopObject(v)
    end
    pDict
  end

  # Convert a Ruby Fixnum to a \Python Int. Returns an FFI::Pointer to a
  # PyIntObject.
  def self.rtopFixnum(rNum)
    RubyPython::Python.PyInt_FromLong(rNum)
  end

  # Convert a Ruby Bignum to a \Python Long. Returns an FFI::Pointer to a
  # PyLongObject.
  def self.rtopBigNum(rNum)
    RubyPython::Python.PyLong_FromLong(rNum)
  end

  # Convert a Ruby float to a \Python Float. Returns an FFI::Pointer to a
  # PyFloatObject.
  def self.rtopFloat(rNum)
    RubyPython::Python.PyFloat_FromDouble(rNum)
  end

  # Returns a \Python False value (equivalent to Ruby's +false+). Returns an
  # FFI::Pointer to Py_ZeroStruct.
  def self.rtopFalse
    RubyPython::Macros.Py_RETURN_FALSE
  end

  # Returns a \Python True value (equivalent to Ruby's +true+). Returns an
  # FFI::Pointer to Py_TrueStruct.
  def self.rtopTrue
    RubyPython::Macros.Py_RETURN_TRUE
  end

  # Returns a \Python None value (equivalent to Ruby's +nil+). Returns an
  # FFI::Pointer to Py_NoneStruct.
  def self.rtopNone
    RubyPython::Macros.Py_RETURN_NONE
  end

  # Convert a Ruby Symbol to a \Python String. Returns an FFI::Pointer to a
  # PyStringObject.
  def self.rtopSymbol(rSymbol)
    RubyPython::Python.PyString_FromString rSymbol.to_s
  end

  # Convert a Ruby Proc to a \Python Function. Returns an FFI::Pointer to a
  # PyCFunction.
  def self.rtopFunction(rObj)
    proc = FFI::Function.new(:pointer, [:pointer, :pointer]) do |p_self, p_args|
      retval = rObj.call(*ptorTuple(p_args))
      pObject = retval.is_a?(RubyPython::RubyPyProxy) ? retval.pObject : RubyPython::PyObject.new(retval)

      # make sure the refcount is >1 when pObject is destroyed
      pObject.xIncref
      pObject.pointer
    end

    defn = RubyPython::Python::PyMethodDef.new
    defn[:ml_name] = FFI::MemoryPointer.from_string("RubyPython::Proc::%s" % rObj.object_id)
    defn[:ml_meth] = proc
    defn[:ml_flags] = RubyPython::Python::METH_VARARGS
    defn[:ml_doc] = nil

    return RubyPython::Python.PyCFunction_New(defn, nil)
  end

  # This will attempt to convert a Ruby object to an equivalent \Python
  # native type. Returns an FFI::Pointer to a \Python object (the
  # appropriate Py…Object C structure). If the conversion is unsuccessful,
  # will raise UnsupportedConversion.
  #
  # [rObj]    A native Ruby object.
  # [is_key]  Set to +true+ if the provided Ruby object will be used as a
  #           key in a \Python +dict+. (This primarily matters for Array
  #           conversion.)
  def self.rtopObject(rObj, is_key = false)
    case rObj
    when String
      rtopString rObj
    when RubyPython::Tuple
      rtopArrayToTuple rObj
    when Array
      # If this object is going to be used as a hash key we should make it a
      # tuple instead of a list
      if is_key
        rtopArrayToTuple rObj
      else
        rtopArrayToList rObj
      end
    when Hash
      rtopHash rObj
    when Fixnum
      rtopFixnum rObj
    when Bignum
      rtopBignum rObj
    when Float
      rtopFloat rObj
    when true
      rtopTrue
    when false
      rtopFalse
    when Symbol
      rtopSymbol rObj
    when Proc, Method
      if RubyPython.legacy_mode
        raise UnsupportedConversion.new("Callbacks are not supported in Legacy Mode.")
      end
      rtopFunction rObj
    when Method
      rtopFunction rObj
    when nil
      rtopNone
    when RubyPython::PyObject
      rObj.pointer
    else
      raise UnsupportedConversion.new("Unsupported type #{rObj.class} for conversion.")
    end
  end

  # Convert an FFI::Pointer to a \Python String (PyStringObject) to a Ruby
  # String.
  def self.ptorString(pString)
    strPtr  = FFI::MemoryPointer.new(:pointer)
    sizePtr = FFI::MemoryPointer.new(:ssize_t)

    RubyPython::Python.PyString_AsStringAndSize(pString, strPtr, sizePtr)

    size = case FFI.find_type(:ssize_t)
           when FFI.find_type(:long)
             sizePtr.read_long
           when FFI.find_type(:int)
             sizePtr.read_int
           when FFI.find_type(:long_long)
             sizePtr.read_long_long
           else
             nil
           end

    strPtr.read_pointer.read_string(size)
  end

  # Convert an FFI::Pointer to a \Python List (PyListObject) to a Ruby
  # Array.
  def self.ptorList(pList)
    rb_array = []
    list_size = RubyPython::Python.PyList_Size(pList)

    list_size.times do |i|
      element = RubyPython::Python.PyList_GetItem(pList, i)
      # PyList_GetItem returns borrowed ref
      RubyPython::Python.Py_IncRef element
      rObject = ptorObject(element)
      rb_array.push rObject
    end

    rb_array
  end

  # Convert an FFI::Pointer to a \Python Int (PyIntObject) to a Ruby Fixnum.
  def self.ptorInt(pNum)
    RubyPython::Python.PyInt_AsLong(pNum)
  end

  # Convert an FFI::Pointer to a \Python Long (PyLongObject) to a Ruby
  # Fixnum. This version does not do overflow checking, but probably should.
  def self.ptorLong(pNum)
    RubyPython::Python.PyLong_AsLong(pNum)
    # TODO Overflow Checking
  end

  # Convert an FFI::Pointer to a \Python Float (PyFloatObject) to a Ruby
  # Float.
  def self.ptorFloat(pNum)
    RubyPython::Python.PyFloat_AsDouble(pNum)
  end

  # Convert an FFI::Pointer to a \Python Tuple (PyTupleObject) to an
  # instance of RubyPython::Tuple, a subclass of the Ruby Array class.
  def self.ptorTuple(pTuple)
    pList = RubyPython::Python.PySequence_List pTuple
    rArray = ptorList pList
    RubyPython::Python.Py_DecRef pList
    RubyPython::Tuple.tuple(rArray)
  end

  # Convert an FFI::Pointer to a \Python Dictionary (PyDictObject) to a Ruby
  # Hash.
  def self.ptorDict(pDict)
    rb_hash = {}

    pos = FFI::MemoryPointer.new :ssize_t
    pos.write_int 0
    key = FFI::MemoryPointer.new :pointer
    val = FFI::MemoryPointer.new :pointer

    while RubyPython::Python.PyDict_Next(pDict, pos, key, val) != 0
      pKey = key.read_pointer
      pVal = val.read_pointer
      rKey = ptorObject(pKey)
      rVal = ptorObject(pVal)
      rb_hash[rKey] = rVal
    end

    rb_hash
  end

  # Converts a pointer to a \Python object into a native Ruby type, if
  # possible. If the conversion cannot be done, the Python object will be
  # returned unmodified.
  #
  # [pObj]  An FFI::Pointer to a \Python object.
  def self.ptorObject(pObj)
    if RubyPython::Macros.PyObject_TypeCheck(pObj, RubyPython::Python.PyString_Type.to_ptr) != 0
      ptorString pObj
    elsif RubyPython::Macros.PyObject_TypeCheck(pObj, RubyPython::Python.PyList_Type.to_ptr) != 0
      ptorList pObj
    elsif RubyPython::Macros.PyObject_TypeCheck(pObj, RubyPython::Python.PyInt_Type.to_ptr) != 0
      ptorInt pObj
    elsif RubyPython::Macros.PyObject_TypeCheck(pObj, RubyPython::Python.PyLong_Type.to_ptr) != 0
      ptorLong pObj
    elsif RubyPython::Macros.PyObject_TypeCheck(pObj, RubyPython::Python.PyFloat_Type.to_ptr) != 0
      ptorFloat pObj
    elsif RubyPython::Macros.PyObject_TypeCheck(pObj, RubyPython::Python.PyTuple_Type.to_ptr) != 0
      ptorTuple pObj
    elsif RubyPython::Macros.PyObject_TypeCheck(pObj, RubyPython::Python.PyDict_Type.to_ptr) != 0
      ptorDict pObj
    elsif pObj == RubyPython::Macros.Py_True
      true
    elsif pObj == RubyPython::Macros.Py_False
      false
    elsif pObj == RubyPython::Macros.Py_None
      nil
    else
      pObj
    end
  end
end