Writing Python code for FileMaker

A detailed description of how to accept a call from FileMaker, important notes about FileMaker parameters, and different ways to return the result.

How it works from FileMaker point of view#Back to top

For FileMaker a plug-in is a library that has a function with a known name. FileMaker calls the function to initialize the plug-in, deinitialize it, notify it about idle time, or ask it to display the configuration dialog, if this is enabled (in PyFM it is not).

A plug-in can also register functions for FileMaker to call: it gives FileMaker the name and the prototype of the function so it can display it in the list of functions and a few other bits of information, such as how many parameters to expect and whether to enable this function on server.

So, aside from initialization and deinitialization there are two real interaction calls:

  • Calling a function.

  • Notifying about idle time.

PyFM supports both these calls.

How it works from the embedded Python point of view#Back to top

At startup PyFM initializes Python and performs other due tasks, such as registering the functions. Then it waits until FileMaker calls one of its functions.

All user code is supposed to be called via PyRun. The function receives module and object name as parameters, import this module and calls the object. For user code it's just that: the code sits somewhere and then something loads it and calls one of its functions. This is where you do most of the interaction.

Receiving a call#Back to top

You will receive a call according to the function signature. If the passed parameters do not match the signature, Python will raise an error itself. The function signature can be anything, except that some combinations won't work:

def myfunc(a, b)
def myfunc(a, b, c=1)
def myfunc(a, b, c=1, *ds)

You can also specify arbitrary keywords, i.e. like this:

def myfunc(a, b, c=1, *ds, **es)

but you'll never get them from FileMaker, because FileMaker only supports positional parameters.

Parameters#Back to top

You will receive native FileMaker data types in a thin Python wrapper. The types you can receive are same as field types: Text, Number, Date, Time, Timestamp, and Container. FileMaker also has other types, such as Style or Environment, but you cannot receive them as parameters.

For pragmatic reasons I had to make filemaker types somewhat unlike common Python classes: first, they can be read-only, and second, they can be volatile.

First, everything you get from FileMaker is read-only. This includes the function parameters and also everything you get from evaluate() and execute_sql_2() functions. This is pretty understandable: a FileMaker function can never have any side effects, the only result of the function is what it returns.

Second, the function parameters are also volatile (but results of evaluate() and execute_sql_2() are not). This means you should not store them between the calls because the parameter you receive may be constructed right before the call and will be destroyed immediately after that. If you store it in global storage and then try to reuse, e.g. to return into FileMaker, it may crash, because the data are no longer there.

This only applies to what to receive from FileMaker. If you construct a FileMaker object programmatically, it will be modifiable and non-volatile. Thus, if you want to edit and/or store the received parameters, all you need to do is to create a new instance of that type and initialize it with the received parameter:

def myfunc(a):      # a is read-only and volatile
    b = type(a)(a)  # b is a modifiable stable copy of a

What happens here is that we get the type of the passed parameter a with the type() function and immediately call the type with a as the parameter. As a result we get a new instance of this type with the same data.

Making a copy is an expensive operation, so only use it if you need to modify the object or store it between the calls. Also, a copy will only have the data of the primary type, not all the data. That is in FileMaker a numeric field can have non-numeric data; for example, it's not a problem to enter ABC 123. The numeric value of this is, naturally 123. When you receive such a number in your function you can still retrieve the original text data by using the following call:

def myfunc(a):                  # a is assumed to be a number
    b = Text(a, original=True)  # b is set to original text of a

But if you make a copy as above, the copy will be a pure number without the original text:

def myfunc(a):                  # a is assumed to be a number
    b = type(a)(a)              # b is a pure number now
    c = Text(b, original=True)  # this will raise an error

This is especially important for containers, because a reference-only container has no container data at all and stores text in the text stream. You have to get it as text to get the reference, but if you copy it, the reference is gone.

Results#Back to top

Your functions doesn't have to return anything, if it's not supposed to. Internally Python will return None and the plug-in will convert it to an empty value.

Your function can return any compatible type; that is not only native FileMaker types like Text or Container, but also any compatible Python types: str, unicode, int, long, float, datetime.date, datetime.time, datetime.datetime, datetime.timedelta, or an explicit None.

More ways to return results#Back to top

If you need to return a lot of data, consider the following alternative methods:

  • You can return results by setting variables. This is especially convenient in scripts: a single call to your function will set the variables right there in script and they will clear when the script exits. I use this in the Python console in PyFM tools: I call the console function and it returns the console output, the prompt, the number of defined symbols and then names, types, and values of each symbol; for the latter I use repeating variables. All I need to do is to put these data into correct fields.

  • You can use the SQL API. Unlike the limited ExecuteSQL() function in v12 the built-in API can insert, delete, and update records and it operates with native types (partially; there are troubles with containers, but there's also a workaround).

  • If you need to run a long operation, you can start a thread, prepare the data asynchronously, and then either call a script using the run_script function or use the run_when_idle to process the data during idle time.

    For now it's mostly a theoretical possibility, because the thread must be friendly and close as soon as possible during shutdown. I think I'll need to provide a special thread class.

What's next#Back to top

The Handling errors explains how to handle errors.