Взаимодействие Python-Ruby.
Декабрь 8, 2009 – 10:04 дпПосле внедрения одной компоненты процессинговой системы, упоминаемой в предыдущей заметке, настало время её использования – надо набивать её мясом, писать скрипты, которые собственно и будут выполнять всю работу. Сказано сделано – к моменту внедрения был написан скрипт для общения с одной внешней системой, через неделю – ещё один. В результате за две недели было проведено около четверти миллиона бизнес-транзакций и около миллиона запросов во внешний мир.
Все было хорошо, пока не потребовалось реализовать скрипт с использованием SOAP протокола. Вариант с кодогенерацией, как в случае использования утилиты wsdl.exe, не подошел, так как данная компонента должна быть постоянно в работе (останов недопустим), так же она должна иметь возможность работать со многими дополнительными частями (модулями), изменяющимися на ходу. Поэтому было принято решение об использовании DLR.
Для решения поставленной задачи были подняты более-менее поддерживаемые python-библиотеки:
- SOAPpy
- soaplib
- ZSI
Однако ни одны из этого списка работать под IronPython не захотела. Только SOAPpy удалось заставить только пропарсить WSDL и сгенерировать клиентов – посылать запросы не получилось.
Все было бы совсем плохо, если бы не DLR (Dynamic Language Runtime), который позволяет работать разным скриптовым языкам в рамках одной платформы. Совершенно случайно было замечено, что в стандартной библиотеке IronRuby имеется soap-клиент. Решили попробовать применить данную библиотеку, уповая на то, что разработчики все-таки выполнили основную задачу проекта DLR – прозрачное взаимодействие разных скриптовых языков.
Вариантов решения данной проблемы было два:
1. Внутри python-скрипта поднимать дополнительный ScriptRuntime и пользоваться IronRuby через DLR.
2. Попробовать использовать механизмы, предусмотренные спецификациями python-модуля clr.
Первый вариант сразу кажется не слишком правильным подходом, т.к. python-скрипт уже работает в рамках одного экземпляра ScriptRuntime. А при внимательном рассматривании модуля clr удалось найти метод Use() со следующим описанием:
Use(name) -> module
Attempts to load the specified module searching all languages in the loaded ScriptRuntime.
Первая попытка использования данного подхода провалилась – clr.Use либо не хотел находить файл по указанному пути, либо после окончания своей работы возвращал нечто и как им можно было пользоваться совершенно не понятно. Было написано письмо в список рассылки. Ответ пришел от Tomas Matousek:
Our Python-Ruby interop is not quite done yet so you need to use some workarounds.
The easiest way how to get WSDL factory instance would be to write a simple Ruby script that loads it:== wsdl.rb ==
require 'soap/wsdlDriver' def get_factory SOAP::WSDLDriverFactory end===
And then you can do:
>>> import clr
>>> wsdl = clr.Use(‘wsdl.rb’, ‘rb’)
>>> factory = wsdl.get_factory()
>>> print factory(“x.wsdl”)
Traceback (most recent call last):
File ““, line 1, in
SystemError: Cannot connect to x.wsdl (Not HTTP.)get_factory method is exposed on the “wsdl” module so that Python can call it. IronRuby doesn’t yet implement dynamic protocols for constant access so you need the get_factory helper for accessing Ruby constants.
Also we have some work to do to make clr.Use work better with libraries of other DLR languages.
Let us know if you hit some other issues.Tomas
Следующий эксперимент показал, что предложенный пример работает исправно. Так началась попытка использования SOAP-клиента из стандартной библиотеки Ruby.
Первым делом был написан следующий ruby-скрипт.
def evaluation ( name )
eval( String( name ) )
end
С помощью данного метода в два счета подключался любой другой ruby-модуль, брался любой класс из подключенных модулей. Вызов конструкторов так же весьма прозрачен, так что с конструированием классов проблем не возникало. Однако разработчики DLR не были бы собой, если бы проблем не было вообще:
1. У объектов Ruby невозможно вызвать некоторые методы. Причем не все, а только некоторые. Почему – непонятно, надо разбираться. При этому операцией dir(obj) методы видны, а вызвать все-равно не получается. Данную проблему можно обойти посредством операции getattr(obj, method_name).
2. Вызов методов. В Python именованные параметры методов – всего-лишь литералы. Однако в Ruby именованных параметров нету – там именованные параметры будут упакованы в объект типа Hash и в таком виде переданы в качестве одного параметра. И кроме того, так как в Ruby абсолютно все является объектом, то и ключи этого хеша тоже являются объектами. В случае Python-варианта именованных параметров, данные литералы наиболее близки к объектам типа Symbol.
В результате различного рода экспериментов родился небольшой набор вспомогательных классов и методов, предназначенный для взаимодействия IronPython c IronRuby.
#-*- coding: utf8 -*-
''' @author: Pavel Suhotyuk
@contact: pavel.suhotjuk@gmail.com
@summary: Helpers for Ruby interop.
'''
import clr
from types import StringTypes, NoneType
#******************************************************************
# Source code
#******************************************************************
rhelper = clr.Use( 'rhelper' )
RubyMethod = rhelper.evaluation( 'Method' )
RubyObject = rhelper.evaluation( 'Object' )
RubyClass = rhelper.evaluation( 'Class' )
MutableString = rhelper.evaluation( 'String' )
Hash = rhelper.evaluation( 'Hash' )
Array = rhelper.evaluation( 'Array' )
Fixnum = rhelper.evaluation( 'Fixnum' )
Integer = rhelper.evaluation( 'Integer' )
Bignum = rhelper.evaluation( 'Bignum' )
Float = rhelper.evaluation( 'Float' )
Symbol = rhelper.evaluation( 'Symbol' )
class RObject ( object ):
def __init__( self, obj ) :
if obj is None :
raise ValueError( 'obj must be not None.' )
self.robj = obj
def __getattr__ ( self, attr_name ) :
if attr_name.startswith( '_get_' ) :
return RMethod( getattr( self.robj, attr_name[5:] ) )
elif attr_name.startswith( '_set_' ) :
return RMethod( getattr( self.robj, '%s=' % attr_name[5:] ) )
else :
return RMethod( getattr( self.robj, attr_name ) )
@property
def to_ruby ( self ):
'''Get Ruby object instance'''
return self.robj
class RMethod ( object ):
'''Ruby method wrapper'''
def __init__( self, method ):
if not ruby_isinstance( method, RubyMethod ) :
raise TypeError( 'Method name must be RubyMethod' )
self.rmethod = method
def __call__ ( self, *args, **kwds ):
if len( kwds ) != 0 :
args = list( args )
args.append( ConvertType.python2ruby( kwds ) )
return ConvertType.ruby2python( self.rmethod( *args ) )
@property
def to_ruby ( self ):
'''Get Ruby method instance'''
return self.rmethod
class RProperty ( object ):
''' Property helper '''
@staticmethod
def create ( robj, prop_name ):
if robj is None :
raise ValueError( 'robj must be not None' )
if prop_name is None :
raise ValueError( 'property name must not None' )
getter = RMethod( getattr( robj, prop_name ) )
setter = RMethod( getattr( robj, RProperty.get_setter_name( prop_name ) ) )
return property( getter, setter, None, robj.__doc__ )
@staticmethod
def is_property ( robj, attr_name ):
return hasattr( robj, RProperty.get_setter_name( attr_name ) )
@staticmethod
def get_setter_name ( attr_name ):
return '%s=' % attr_name
class RClass ( object ):
'''Ruby class wrapper'''
def __init__( self, rcls ):
self.ruby_cls = rcls
def __call__ ( self, *args, **kwds ):
''' Call constructor '''
if len( kwds ) != 0 :
args = list( args )
args.append( ConvertType.python2ruby( kwds ) )
return RObject( self.ruby_cls( *args ) )
@property
def to_ruby ( self ):
'''Get Ruby class instance '''
return self.ruby_cls
def _import_ ( module ) :
'''Import Ruby module'''
rhelper.evaluation( "require '%s'" % module )
def get_class( cls ):
'''Get wrapped Ruby class '''
if isinstance( cls, StringTypes ) :
return RClass( rhelper.evaluation( cls ) )
elif ruby_isinstance( cls, RubyClass ) :
return RClass( cls )
else :
raise TypeError( 'cls parameter must be string or RubyClass.' )
class ConvertType ( object ):
'''Helpers for convert python types to ruby.'''
@staticmethod
def python2ruby( value ):
if ConvertType._python_is_simple_type( value ) :
return ConvertType._python_parse_simple( value )
elif ConvertType._python_is_complex_type( value ) :
return ConvertType._python_parse_complex( value )
@staticmethod
def ruby2python( value ):
if ConvertType._ruby_is_simple_type( value ) :
return ConvertType._ruby_parse_simple( value )
elif ConvertType._ruby_is_complex_type( value ) :
return ConvertType._ruby_parse_complex( value )
@staticmethod
def _python_parse_simple( value ):
if isinstance( value, basestring ) :
return ConvertType._str2MutableString( value )
else :
return value
@staticmethod
def _python_parse_complex ( value ):
if isinstance( value, ( tuple, list ) ) :
return ConvertType._list2array( value )
elif isinstance( value, dict ) :
return ConvertType._dict2hash( value )
else :
return value
@staticmethod
def _ruby_parse_simple( value ):
if ruby_isinstance( value, Fixnum ) or ruby_isinstance( value, Integer ) :
return int( value )
elif ruby_isinstance( value, Bignum ) :
return long( value )
elif ruby_isinstance( value, Float ) :
return float( value )
elif ruby_isinstance( value, MutableString ) or ruby_isinstance( value, Symbol ) :
return str( value )
@staticmethod
def _ruby_parse_complex( value ):
if ruby_isinstance( value, Hash ) :
return ConvertType._hash2dict( value )
elif ruby_isinstance( value, Array ) :
return ConvertType._array2list( value )
elif ruby_isinstance( value, RubyClass ) :
return ConvertType._rubyClass2pythonClass( value )
elif ruby_isinstance( value, RubyMethod ) :
return ConvertType._rubyMethod2pythonMethod( value )
else :
return ConvertType._rubyObject2pythonObject( value )
@staticmethod
def _rubyMethod2pythonMethod( value ):
return RMethod( value )
@staticmethod
def _rubyClass2pythonClass( value ):
return RClass( value )
@staticmethod
def _rubyObject2pythonObject ( value ):
return RObject( value )
@staticmethod
def _hash2dict ( value ):
return dict( [ ( ConvertType.ruby2python( pair.Key ), ConvertType.ruby2python( pair.Value ) ) for pair in value ] )
@staticmethod
def _array2list( value ):
return [ ConvertType.ruby2python( item ) for item in value ]
@staticmethod
def _ruby_is_simple_type( value ):
return ( ruby_isinstance( value, Fixnum ) or
ruby_isinstance( value, Bignum ) or
ruby_isinstance( value, Integer ) or
ruby_isinstance( value, Float ) or
ruby_isinstance( value, Symbol ) or
ruby_isinstance( value, MutableString ) )
@staticmethod
def _ruby_is_complex_type( value ):
return ( ruby_isinstance( value, Hash ) or
ruby_isinstance( value, Array ) or
ruby_isinstance( value, RubyObject ) or
ruby_isinstance( value, RubyClass ) or
ruby_isinstance( value, RubyMethod ) )
@staticmethod
def _python_is_complex_type( obj ):
return isinstance( obj, ( object, tuple, list, dict ) )
@staticmethod
def _python_is_simple_type( obj ) :
return isinstance( obj, ( int, long, float, bool, NoneType, basestring ) )
@staticmethod
def _dict2hash ( params ):
''' Convert Python dict to Ruby Hash '''
rhash = Hash()
for key in params :
rhash.Add( ConvertType.python2ruby( key ), ConvertType.python2ruby( params[key] ) )
return rhash
@staticmethod
def _list2array ( value ):
return Array( [ ConvertType.python2ruby( v ) for v in value ] )
@staticmethod
def _str2MutableString( val ):
return MutableString( val )
def ruby_isinstance( obj, cls ):
return getattr( cls, '===' )( obj )
Данные вспомогательные классы и методы не претендуют на универсальность – они писались для конкретной задачи (взаимодействие python-скриптов с soap ruby-библиотекой). Если данная связка в проекте будет и дальше использоваться, придется расширять этот набор методов и решать проблемы с конвертацией типов результатов методов и при вызове.
Пример использования:
from ruby import _import_, get_class
_import_('soap/wsdlDriver')
Soap = get_class('SOAP::WSDLDriverFactory')
client = Soap('http://localhost/Service1.asmx?WSDL').create_rpc_driver()
client.HelloWorld(None)
Так же хотелось бы заметить, что стандартные сборки IronPython и IronRuby, выложенные на сайтах проектов, работают с разными версиями DLR, поэтому без небольшой работы заставить их работать вместе в одном AppDomain не получится. Для этого были взяты и собраны исходные коды проекта. Однако и это не все – получаемые при этом сборки консолей IronPython и IronRuby не хотят работать. Почему – мне уже не так интересно, так как задача стояла использовать DLR как встраиваемую среду, так что для отладки и тестирования была собрана собственная Python консоль с интерпретаторами Python и Ruby. Здесь можно взять полную версию проекта.
Скорее всего в дальнейшем будет выделенно время для адаптации какой-либо python-библиотеки под IronPython (скорее всего soaplib), однако сейчас приходится пользоваться данными костылями.