Theory

To understand how to use modules under Eiffel, we have to understand how they are typically used under Python. Unlike Eiffel, Python uses an explicit "import" directive to import the qualified contents of a module into the local namespace; for example, to import only the compile function from the re module, one would use the following code:

Example 10-1. Importing a single function from a module in Python

Python 1.5.2 (#1, Sep 17 1999, 20:15:36)  [GCC egcs-2.91.66 19990314/Linux (egcs- on linux-i386
Copyright 1991-1995 Stichting Mathematisch Centrum, Amsterdam
>>> from re import compile
>>> r = compile( r"^\D*(\d*)" )

This sort of selective importing is seen in a fair amount of Python code, probably because it is perceived as inefficient and clumsy to qualify the names completely every time they are used. However, to avoid polluting the common namespace with unqualified names, the other method of importing entire modules at once can be extremely handy:

Example 10-2. Importing a module at once in Python

Python 1.5.2 (#1, Sep 17 1999, 20:15:36)  [GCC egcs-2.91.66 19990314/Linux (egcs- on linux-i386
Copyright 1991-1995 Stichting Mathematisch Centrum, Amsterdam
>>> import re
>>> r = re.compile( r"^\D*(\d*)" )

It is this latter form of import which concerns us, because of the way the module is used when it is imported. In fact, it's safe to say that the module is treated exactly as a reference to a very large object with relatively little state-dependent features and a great deal of exported functions. If we start to think of a module as a single instance of a class which derives from an overall class module, we are well on our way to creating a solution usable from within Eiffel.

In fact, our primitives even include a relatively useful place to start, the class PYTHON_MODULE, which can be used to import Python modules and provide some sort of interface to them. At the moment, PYTHON_MODULE is fairly limited, providing only features to create itself, import itself from a module, and get the dictionary of features it provides. However, we can extend it considerably if we use the same concepts that we used to create the PYTHON_USER_CLASS class from the last section, and extend its functionality likewise; in other words, to call a function that resides in a module, we must:

  1. The Python module in question must be located and imported

  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

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

This looks extremely similar to the earlier case of creating and using a user-defined Python class, and that's very true--the process is extremely similar. The only real differences, in fact, come from the fact that we are locating and importing a module instead of a class, and that we don't need to call the function in question using the object as the first argument (the "self" argument). To assist in the creation of user-defined modules, it makes sense to create a PYTHON_USER_MODULE class which takes over some of the housekeeping details:

Example 10-3. Short form of the PYTHON_USER_MODULE class

deferred class interface PYTHON_USER_MODULE
feature(s) from PYTHON_USER_MODULE
   module_name: STRING
   make
      -- initialize using module_name
      require
         module_name /= Void
   from_import (new_module_name: STRING)
      -- imports the module--for user-defined modules, this must be 
      -- equal to the feature "module_name" or loading will not work
      require
         new_module_name.is_equal(module_name)
   function (function_name: STRING): PYTHON_OBJECT
      -- get the callable function of the given name
      require
         function_name /= Void;
         has_attr_string(function_name)
      ensure
         Result.is_callable
   call_function (function_name: STRING; function_args: PYTHON_TUPLE): PYTHON_OBJECT
      require
         function_name /= Void
invariant
   reference_count >= 0;
   valid_reference: not is_none implies is_module;
end of deferred PYTHON_USER_MODULE

As with PYTHON_USER_CLASS, PYTHON_USER_MODULE is a deferred class, meaning that it cannot be instantiated directly. It undefines the from_name feature of PYTHON_MODULE, and redefines from_import so that it adds the requirement

 new_module_name.is_equal( module_name )

...this is done to ensure that the user module object is never initialized with an incorrect module name, which would result in extremely cryptic errors.

We also add a new feature, function, which looks up the requested name in the module's symbol table and returns it as a PYTHON_OBJECT (and, by virtue of its postcondition, ensures that it is a callable object, so that we do not get data attributes by mistake). A related function, call_function, allows a simple interface to calling module-owned functions--provide the name of the function and a tuple of arguments, and call_function will look up the function and call it all in one step.

As we did with user classes, we consider data attributes as nothing special--generate a getter function, but rather than using setters, we really rely on the PYTHON_OBJECT.set_attr* family of functions, and their friends, to handle attribute modification.

To make a usable module, then, we must redefine module_name and provide a set of named Eiffel features to export each of these to Eiffel, while under the hood using PYTHON_USER_MODULE.call_function to locate the desired feature and call it. And, as it was with user-defined classes, the process of doing all this by hand is tedious and error-prone.