Theory: User-defined classes

The answer once again lies in the use of shadow classes--Eiffel classes that defer feature calls through the Python/C API and an opaque pointer to a Python object. The marshalling is somewhat complex, because such a feature call has do do considerable shuffling:

  1. The Python class in question must be located and established as a class object

  2. The desired feature must be looked up and found as a callable Python function

  3. The arguments to be passed to the function must be converted into a Python tuple of Python objects, with a pointer to the object as the first argument (the "self" argument)

  4. The function must then be called with the tuple of Python objects as its only argument.

This is a fair amount of work to do, but not insurmountable. In fact, it's possible to do all of the above by hand--but that would be inefficient and error-prone. We'll start by automating some of the above steps, mostly the lookup and conversion problems, by creating a base class for user-defined Python classes, named appropriately PYTHON_USER_CLASS, which inherits from PYTHON_CLIENT_WRAPPER; the short form is shown below:

Example 9-1. The short form of PYTHON_USER_CLASS

deferred class interface PYTHON_USER_CLASS
feature(s) from PYTHON_USER_CLASS
   module_name: STRING
   class_name: STRING
   class_object: PYTHON_OBJECT
   method (method_name: STRING): PYTHON_OBJECT
      -- get the callable method of the requested name
      require
         method_name /= Void;
         class_object.has_attr_string(method_name)
      ensure
         Result.is_callable
   self_tuple: PYTHON_TUPLE
      ensure
         last_python_integer_result_ok
   call_method (method_name: STRING; args: PYTHON_TUPLE): PYTHON_OBJECT
      require
         method_name /= Void
   from_init (args: PYTHON_TUPLE)
      -- initializes the object using the class object defined by 
      -- feature "class_object"--desired creation clause
invariant
   reference_count >= 0;
end of deferred PYTHON_USER_CLASS

The from_init method does quite a bit of the work, loading the requested module and initializing the class_object member to the Python object representing the desired class. It then calls that class's __init__ function to prepare the object for use. To find that method (or any method), the method function uses the get_attr_string feature of PYTHON_OBJECT to get the desired function object from the class's class_object. Finally, the call_method feature bundles the given arguments into a tuple with the object's pointer and calls the function object to get the desired result.

Deriving a user-defined class, then, is relatively painless, but somewhat tedious: derive from PYTHON_USER_CLASS, and redefine module_name and class_name to point to the new class; if I were to use the Python class smtplib.SMTP, I would redefine these as follows:

Example 9-2. Redefinition of module_name and class_name in a theoretical user-defined class

   module_name : STRING is once Result := "smtplib" end;
   class_name : STRING is once Result := "SMTP" end;

Then, for each feature in the Python class, create an analogous feature in the new class which calls call_method with the function name and a tuple of arguments. For convenience, you could also use PYTHON_OBJECT_FACTORY's new_python_tuple function to pass an array of Eiffel objects rather than having to create each tuple of arguments by hand--a tremendous timesaver. We adopted the convention of naming the Eiffel feature which accepts an array of Eiffel arguments after the Python feature of the same name, but also included the same feature (accepting a PYTHON_TUPLE as arguments) postpended with "_python" as well. For example, the sendmail function of the Python smtplib.SMTP would show up in the new class as so:

Example 9-3. The smtplib.SMTP.sendmail feature, as it would appear in the Eiffel class

   sendmail( args : ARRAY[ ANY ] ) : PYTHON_OBJECT is
      do
         Result := call_method( "sendmail", python_object_factory.new_python_tuple( args ) )
      end
      
   sendmail_python( args : PYTHON_TUPLE ) : PYTHON_OBJECT is
      do
         Result := call_method( "sendmail", args )
      end

Really, that's all there is to it--it just involves a lot of software development elbow grease. While most developers are good at working hard, it's much more efficient to be lazy and teach the computer how to do this--and for a quick script to generate code text, Python is one of the reigning champions.