lib/rubypython/rubypyproxy.rb in rubypython-0.3.2 vs lib/rubypython/rubypyproxy.rb in rubypython-0.5.0

- old
+ new

@@ -3,87 +3,98 @@ require 'rubypython/conversion' require 'rubypython/operators' require 'rubypython/blankobject' module RubyPython - #This is the object that the end user will most often be interacting - #with. It holds a reference to an object in the Python VM an delegates - #method calls to it, wrapping and returning the results. The user should - #not worry about reference counting of this object an instance - #will decrement its objects reference count when it is garbage collected. + # In most cases, users will interact with RubyPyProxy objects that hold + # references to active objects in the \Python interpreter. RubyPyProxy + # delegates method calls to \Python objects, wrapping and returning the + # results as RubyPyProxy objects. # - #Note: All RubyPyProxy objects become invalid when the Python interpreter - #is halted. + # The allocation, deallocation, and reference counting on RubyPyProxy + # objects is automatic: RubyPython takes care of it all. When the object + # is garbage collected, the instance will automatically decrement its + # object reference count. # - #Calling Methods With Blocks - #----------------------------- - #Any method which is forwarded to a Python object may be called with - #a block. The result of the method will passed as the argument to - #that block. + # [NOTE:] All RubyPyProxy objects become invalid when the \Python + # interpreter is halted. # - #@example Supplying a block to a method call - # irb(main):001:0> RubyPython.start - # => true - # irb(main):002:0> RubyPython::PyMain.float(10) do |f| - # irb(main):003:1* 2*f.rubify - # irb(main):004:1> end - # => 20.0 - # irb(main):005:0> RubyPython.stop - # => true + # == Calling Methods With Blocks + # Any method which is forwarded to a \Python object may be called with a + # block. The result of the method will passed as the argument to that + # block. # + # RubyPython.run do + # sys = RubyPython.import 'sys' + # sys.version { |v| v.rubify.split(' ') } + # end + # # => [ "2.6.1", … ] + # + # == Passing Procs and Methods to \Python Methods + # RubyPython supports passing Proc and Method objects to \Python methods. + # The Proc or Method object must be passed explicitly. As seen above, + # supplying a block to a method will result in the return value of the + # method call being passed to the block. # - #Passing Procs and Methods to methods - #------------------------------------- - #RubyPython now supports passing Proc and Method objects to Python - #methods. The Proc or Method object must be passed explicitly. As - #seen above, supplying a block to a method will result in the return - #value of the method call being passed to the block. + # When a Proc or Method is supplied as a callback, then arguments that it + # will be called with will be wrapped \Python objects. It will therefore + # typically be necessary to write a wrapper around any Ruby callback that + # requires native Ruby objects. # - #When a Proc or Method is supplied as a callback, then arguments that - #it will be called with will be wrapped Python objects. + # # Python Code: sample.py + # def apply_callback(callback, argument): + # return callback(argument) # - #@example Passing a Proc to Python - # #Python Code - # def apply_callback(callback, argument): - # return callback(argument) - # - # #IRB Session - # irb(main):001:0> RubyPython.start - # => true - # irb(main):002:0> sys = RubyPython.import 'sys' - # => <module 'sys' (built-in)> - # irb(main):003:0> sys.path.append('.') - # => None - # irb(main):004:0> sample = RubyPython.import 'sample' - # => <module 'sample' from './sample.pyc'> - # irb(main):005:0> callback = Proc.new do |arg| - # irb(main):006:1* arg * 2 - # irb(main):007:1> end - # => #<Proc:0x000001018df490@(irb):5> - # irb(main):008:0> sample.apply_callback(callback, 21).rubify - # => 42 - # irb(main):009:0> RubyPython.stop - # + # # IRB Session + # >> RubyPython.start + # => true + # >> sys = RubyPython.import 'sys' + # => <module 'sys' (built-in)> + # >> sys.path.append('.') + # => None + # >> sample = RubyPython.import 'sample' + # => <module 'sample' from './sample.pyc'> + # >> callback = Proc.new { |arg| arg * 2 } + # => # <Proc:0x000001018df490@(irb):5> + # >> sample.apply_callback(callback, 21).rubify + # => 42 + # >> RubyPython.stop + # => true class RubyPyProxy < BlankObject include Operators attr_reader :pObject + # Creates a \Python proxy for the provided Ruby object. + # + # Only the following Ruby types can be represented in \Python: + # * String + # * Array + # * Hash + # * Fixnum + # * Bignum + # * Float + # * Symbol (as a String) + # * Proc + # * Method + # * +true+ (as True) + # * +false+ (as False) + # * +nil+ (as None) def initialize(pObject) if pObject.kind_of? PyObject @pObject = pObject else @pObject = PyObject.new pObject end end - #Handles the job of wrapping up anything returned by a {RubyPyProxy} - #instance. The behavior differs depending on the value of - #{RubyPython.legacy_mode}. If legacy mode is inactive, every returned - #object is wrapped by an instance of {RubyPyProxy}. If legacy mode is - #active, RubyPython first attempts to convert the returned object to a - #native Ruby type, and then only wraps the object if this fails. + # Handles the job of wrapping up anything returned by a RubyPyProxy + # instance. The behavior differs depending on the value of + # +RubyPython.legacy_mode+. If legacy mode is inactive, every returned + # object is wrapped by an instance of +RubyPyProxy+. If legacy mode is + # active, RubyPython first attempts to convert the returned object to a + # native Ruby type, and then only wraps the object if this fails. def _wrap(pyobject) if pyobject.class? RubyPyClass.new(pyobject) elsif RubyPython.legacy_mode pyobject.rubify @@ -91,137 +102,178 @@ RubyPyProxy.new(pyobject) end rescue Conversion::UnsupportedConversion => exc RubyPyProxy.new pyobject end + private :_wrap reveal(:respond_to?) - #Moves the old respond_to? method to is_real_method? + # The standard Ruby +#respond_to?+ method has been renamed to allow + # RubyPython to query if the proxied \Python object supports the method + # desired. Setter methods (e.g., +foo=+) are always supported. alias :is_real_method? :respond_to? - #RubyPython checks the attribute dictionary of the wrapped object - #to check whether it will respond to a method call. This should not - #return false positives but it may return false negatives. The builitin Ruby - #respond_to? method has been aliased to is_real_method?. + # RubyPython checks the attribute dictionary of the wrapped object to + # check whether it will respond to a method call. This should not return + # false positives but it may return false negatives. The built-in Ruby + # respond_to? method has been aliased to is_real_method?. def respond_to?(mname) return true if is_real_method?(mname) mname = mname.to_s - return true if mname.end_with? '=' + return true if mname =~ /=$/ @pObject.hasAttr(mname) end - #Implements the method call delegation. + # Delegates method calls to proxied \Python objects. + # + # == Delegation Rules + # 1. If the method ends with a question-mark (e.g., +nil?+), it can only + # be a Ruby method on RubyPyProxy. Attempt to reveal it (RubyPyProxy + # is a BlankObject) and call it. + # 2. If the method ends with equals signs (e.g., +value=+) it's a setter + # and we can always set an attribute on a \Python object. + # 3. If the method ends with an exclamation point (e.g., +foo!+) we are + # attempting to call a method with keyword arguments. + # 4. The Python method or value will be called, if it's callable. + # 5. RubyPython will wrap the return value in a RubyPyProxy object + # (unless legacy_mode has been turned on). + # 6. If a block has been provided, the wrapped return value will be + # passed into the block. def method_missing(name, *args, &block) name = name.to_s - if(name.end_with? "?") + if name =~ /\?$/ begin RubyPyProxy.reveal(name.to_sym) return self.__send__(name.to_sym, *args, &block) rescue RuntimeError => exc raise NoMethodError.new(name) if exc.message =~ /Don't know how to reveal/ raise end end + kwargs = false - if(name.end_with? "=") - setter = true - name.chomp! "=" - else - setter=false + if name =~ /=$/ + return @pObject.setAttr(name.chomp('='), + PyObject.convert(*args).first) + elsif name =~ /!$/ + kwargs = true + name.chomp! "!" end - if(!@pObject.hasAttr(name) and !setter) - raise NoMethodError.new(name) - end + raise NoMethodError.new(name) if !@pObject.hasAttr(name) - - args = PyObject.convert(*args) - - if setter - return @pObject.setAttr(name, args[0]) - end - pFunc = @pObject.getAttr(name) if pFunc.callable? if args.empty? and pFunc.class? pReturn = pFunc else + if kwargs and args.last.is_a?(Hash) + pKeywords = PyObject.convert(args.pop).first + end + + orig_args = args + args = PyObject.convert(*args) pTuple = PyObject.buildArgTuple(*args) - pReturn = pFunc.callObject(pTuple) - if(PythonError.error?) - raise PythonError.handle_error + pReturn = if pKeywords + pFunc.callObjectKeywords(pTuple, pKeywords) + else + pFunc.callObject(pTuple) end + + # Clean up unused Python vars instead of waiting on Ruby's GC to + # do it. + pFunc.xDecref + pTuple.xDecref + pKeywords.xDecref if pKeywords + orig_args.each_with_index do |arg, i| + # Only decref objects that were created in PyObject.convert. + if !arg.kind_of?(RubyPython::PyObject) and !arg.kind_of?(RubyPython::RubyPyProxy) + args[i].xDecref + end + end + + raise PythonError.handle_error if PythonError.error? end else pReturn = pFunc end - return _wrap(pReturn) + result = _wrap(pReturn) + + if block + block.call(result) + else + result + end end - #RubyPython will attempt to translate the wrapped object into a native - #Ruby object. This will only succeed for simple builtin type. + # RubyPython will attempt to translate the wrapped object into a native + # Ruby object. This will only succeed for simple built-in type. def rubify @pObject.rubify end - #Returns the string representation of the wrapped object via a call to the - #object's \_\_repr\_\_ method. - # - #@return [String] + # Returns the String representation of the wrapped object via a call to + # the object's <tt>__repr__</tt> method, or the +repr+ method in PyMain. def inspect self.__repr__.rubify rescue PythonError, NoMethodError RubyPython::PyMain.repr(self).rubify end - #Returns the string representation of the wrapped object via a call to the - #object's \_\_str\_\_ method. - # - #@return [String] + # Returns the string representation of the wrapped object via a call to + # the object's <tt>__str__</tt> method or the +str+ method in PyMain. def to_s self.__str__.rubify rescue PythonError, NoMethodError RubyPython::PyMain.str(self).rubify end - #Converts the wrapped Python object to a Ruby Array. Note that this only converts - #one level, so a nested array will remain a proxy object. Only wrapped - #objects which have an \_\_iter\_\_ method may be converted using to_a. + # Converts the wrapped \Python object to a Ruby Array. Note that this + # only converts one level, so a nested array will remain a proxy object. + # Only wrapped objects which have an <tt>__iter__</tt> method may be + # converted using +to_a+. # - #Note that for Dict objects, this method returns what you would get in - #Python, not in Ruby i.e. a\_dict.to\_a returns an array of the - #dictionary's keys. - #@return [Array<RubyPyProxy>] - #@example List - # irb(main):001:0> RubyPython.start - # => true - # irb(main):002:0> a_list = RubyPython::RubyPyProxy.new [1, 'a', 2, 'b'] - # => [1, 'a', 2, 'b'] - # irb(main):003:0> a_list.kind_of? RubyPython::RubyPyProxy - # => true - # irb(main):004:0> a_list.to_a - # => [1, 'a', 2, 'b'] - # irb(main):005:0> RubyPython.stop - # => true - # - #@example Dict - # irb(main):001:0> RubyPython.start - # => true - # irb(main):002:0> a_dict = RubyPython::RubyPyProxy.new({1 => '2', :three => [4,5]}) - # => {1: '2', 'three': [4, 5]} - # irb(main):003:0> a_dict.kind_of? RubyPython::RubyPyProxy - # => true - # irb(main):004:0> a_dict.to_a - # => [1, 'three'] - # irb(main):005:0> RubyPython.stop - # => true + # Note that for \Python Dict objects, this method returns what you would + # get in \Python, not in Ruby: +a_dict.to_a+ returns an array of the + # dictionary's keys. # + # === List #to_a Returns an Array + # >> RubyPython.start + # => true + # >> list = RubyPython::RubyPyProxy.new([1, 'a', 2, 'b']) + # => [1, 'a', 2, 'b'] + # >> list.kind_of? RubyPython::RubyPyProxy + # => true + # >> list.to_a + # => [1, 'a', 2, 'b'] + # >> RubyPython.stop + # => true + # + # === Dict #to_a Returns An Array of Keys + # >> RubyPython.start + # => true + # >> dict = RubyPython::RubyPyProxy.new({1 => '2', :three => [4,5]}) + # => {1: '2', 'three': [4, 5]} + # >> dict.kind_of? RubyPython::RubyPyProxy + # => true + # >> dict.to_a + # => [1, 'three'] + # >> RubyPython.stop + # => true + # + # === Non-Array Values Do Not Convert + # >> RubyPython.start + # => true + # >> item = RubyPython::RubyPyProxy.new(42) + # => 42 + # >> item.to_a + # NoMethodError: __iter__ def to_a iter = self.__iter__ ary = [] loop do ary << iter.next() @@ -229,32 +281,56 @@ rescue PythonError => exc raise if exc.message !~ /StopIteration/ ary end - end + # Returns the methods on the \Python object by calling the +dir+ + # built-in. + def methods + pObject.dir.map { |x| x.to_sym } + end - #A class to wrap Python Modules. It behaves exactly the same as {RubyPyProxy}. - #It is just here for Bookkeeping and aesthetics. - class RubyPyModule < RubyPyProxy + # Creates a PyEnumerable for this object. The object must have the + # <tt>__iter__</tt> method. + def to_enum + PyEnumerable.new(@pObject) + end end - #A class to wrap Python Classes. - class RubyPyClass < RubyPyProxy + # A class to wrap \Python modules. It behaves exactly the same as + # RubyPyProxy. It is just here for Bookkeeping and aesthetics. + class RubyPyModule < RubyPyProxy; end - #Create an instance of the wrapped class. This is a workaround for the fact - #that Python classes are meant to be callable. + # A class to wrap \Python classes. + class RubyPyClass < RubyPyProxy + # Create an instance of the wrapped class. This is a workaround for the + # fact that \Python classes are meant to be callable. def new(*args) args = PyObject.convert(*args) pTuple = PyObject.buildArgTuple(*args) pReturn = @pObject.callObject(pTuple) - if PythonError.error? - raise PythonError.handle_error - end + raise PythonError.handle_error if PythonError.error? RubyPyInstance.new pReturn end end - #An object representing an instance of a Python Class. - class RubyPyInstance < RubyPyProxy + # An object representing an instance of a \Python class. It behaves + # exactly the same as RubyPyProxy. It is just here for Bookkeeping and + # aesthetics. + class RubyPyInstance < RubyPyProxy; end + + # An object representing a Python enumerable object. + class PyEnumerable < RubyPyProxy + include Enumerable + + def each + iter = self.__iter__ + loop do + begin + yield iter.next + rescue RubyPython::PythonError => exc + return if exc.message =~ /StopIteration/ + end + end + end end end