A hands-on tutorial to make a small PyFM extension with three simple functions. Write functions with variable number of parameters, work with FileMaker containers, handle errors, and in the end permanently the extension to the plug-in library.
You are expected to understand FileMaker development and programming in general, ideally the Python language.
The first example is the obligatory “Hello, world” example. Use a text editor to create a text file and enter (or copy and paste) the following:
def hello(): return "Hello, PyFM!"
Save the file on your disk as myext.py
so that you can type or copy the
path to it.
This Python code snippet defines the hello
function; the function takes no
parameters and returns a string: Hello, PyFM!
. The result is a Python
string (str
); the plug-in will automatically convert it into FileMaker
text.
PyLab
Our myext.py
file is a valid Python module myext
. There are two ways
to make it available for the Python interpreter inside the PyFM plug-in:
Add the file to the plug-in library (a Zip archive attached to the plug-in file). This way it becomes a part of the plug-in. We'll try this at the end when our module is ready and tested.
Temporarily registered with the PyLab function. This way the module can be called with PyLab during this session. PyLab will also automatically reload the module if the file changes, which is especially convenient during development.
PyLab is a normal plug-in function so you can call it in the usual FileMaker
way, e.g. from a field, script, from the Watch
tab of the Data Viewer
window, and so on. To make this more convenient, the PyFM.fmp12
file
provides a simple tool to create, store, and evaluate FileMaker expressions.
To use PyLab with this tool open the PyFM.fmp12
file, go to the
Expressions
tab and press the the New expression
button to create a
new expression. Type an expression according to the following syntax:
PyLab( "register:";
)Note that the register:
keyword has a colon at the end; this is
what makes it a keyword.
Press the Evaluate
button to evaluate the expression. If everything is OK,
you'll see Success
as the result and the result type will change to
Text
.
Now the file is registered and you can call its functions.
We're ready to call the function. Again, let's use expressions. Create a new expression, and type:
PyLab( "myext"; "hello" )
The command tells PyLab to find the myext
module (our myext.py
file), call the hello
function from the module, and return the result.
Press the Evaluate
button; if everything is OK, the result will be
Hello, PyFM!
:
Now let's write a bit more sophisticated and practically useful function. The function will take text, file name, and encoding, and return a container with a text file with the same text in the specified encoding. The encoding and file name will be optional. In FileMaker the calls to the function will look so:
PyLab( "myext"; "encode"; ) ;;
If encoding
is omitted, it's assumed to be UTF-8, if filename
is
omitted, it's set to untitled.txt
.
Return to your myext.py
file and enter code for the new function:
from filemaker import Container def encode(text, name='untitled.txt', encoding='utf-8'): data = unicode(text).encode(str(encoding)) return Container(FNAM=name, FILE=data)
Save the file, switch back to PyFM.fmp12
and create a new expression, for
example:
PyLab( "myext"; "encode"; "Sample text"; "sample.txt" )
Press the Evaluate
button; if everything is OK, you'll see a container
result with your file:
Let's see what's going on here:
from filemaker import Container
First we import a Container
type from the filemaker
module; we need
this type to create FileMaker container values.
def encode(text, name='untitled.txt', encoding='utf-8'):
Here we define a function called encode
that takes from one to three
parameters. The first parameter (text
) is required, the other two are
optional; if the user omits them, they will be set to the specified default
values.
The next line creates the file data:
data = unicode(text).encode(str(encoding))
First we convert the received text (which is filemaker.Text
type) to
Python unicode
:
unicode(text)
Then we immediately call the encode()
method of the resulting unicode
object:
unicode(text).encode(...)
The method needs the name of the encoding (see Standard Encodings in
Python documentation), but it has to be a Python string instance, so we
take the supplied encoding and convert it to Python str
:
unicode(text).encode(str(encoding))
The last line creates and returns a Container
with two streams, FNAM
and FILE
, and passes them the name
and data
respectively.
return Container(FNAM=name, FILE=data)
The Container
is a class for FileMaker containers. FileMaker containers
consist of streams; a stream is like an independent piece of data with a
four-character code that identifies what it is. The FNAM
stream is the
name of the file and FILE
stream is the file data. The result is a typical
container that stores a file.
What happens if something goes wrong? For example, what if the system cannot find such an encoding or cannot encode the supplied text using this encoding, or some other error?
Python deals with errors using exceptions. When a function encounters something it cannot handle, it raises an exception. An exception breaks the normal control flow and gets passed up the chain of callers until one of them handles the exception. To handle an exception is to do something about it: report it to the user, choose a different action, ignore, or maybe raise another exception.
Note that exceptions are not errors, they're more like unexpected conditions or situations that are out of depth for a specific function. Since individual functions are usually pretty simple, there's a lot of such situations.
For example, when a function iterates over a list of elements and reaches
the end, this is something it cannot handle by itself; in Python the
function raises an exception (StopIteration
). In most cases you never
see this exception, because the part of the Python interpreter that deals
with iterators knows about this exception and expects it; for this part
the situation is not unexpected, but completely normal.
Similarly, when a function tries to open a file and the file doesn't exist, this is an exception for this function, but for its caller it may just be a notification that it needs to create the file or just do without it.
What happens if nobody handles the exception? This never happens. In every case there's a piece of code at the very top that handles all exceptions. Since this is the last instance, the handling is normally very simple and usually severe. For example, if you run Python from the command line and get an exception that raises to the top, a small piece of code will catch it, print its description to the terminal and stop the process.
In PyFM all Python code is called by the insides of the plug-in. If there's an exception and no code does something about it, then it eventually gets raised to the plug-in level. At this level the plug-in handles all exceptions: it stores the message and other information about the exception and returns control to FileMaker reporting that there was an error. After that it's up to the developer what to do about it.
When a plug-in reports to FileMaker there's an error, FileMaker displays the
result as a question mark (?
; it's interesting that the type of the
result becomes Number
). This is the standard way to test if there was an
error. In many cases a question mark cannot possibly be a valid result of a
function call, so this is a sufficient test:
PyFM also provides the PyLastError function to read the error occurred in the last call to any of the plug-in's functions except PyLastError itself. If there was no error, the function returns an empty string, otherwise it returns a non-empty string, normally the error message.
For example, I can call a plug-in function, check for error, compose an error message, and choose the script branch all in a single calculation:
If a function can technically return a question mark, then you can also test if the result of PyLastError is empty:
PyLastError is written to be as fast as possible; you can safely
keep it in the Watch
tab of the Data Viewer
window.
In the PyFM.fmp12
file the code that evaluates expressions displays the
error as another result. Try to pass a wrong encoding name to the encode
function and you'll get this:
Now let's write yet another useful function to list a field. FileMaker already
has such a function (List()
) and a special summary type that lists
contents of the found set of records, but our function will do something
extra:
It will work both over a related field and over a found set.
It will return the result not as a single list of values, but in repetitions of a variable; in addition it will give us the number of results so we can loop over these repetitions. This way we can, for example, list container fields.
This example will show how to call back to FileMaker to evaluate an expression, intercept a FileMaker error (dressed up as a Python exception), and return data into variables.
The function syntax is that:
PyLab( "myext"; "list"; );
The field-name
parameter is required and must be the name of the field
to list. The variable-name
parameter is optional; if omitted it will be
$list
.
The function will return a number that says how many values we've got and the variable with that many repetitions. The code:
from filemaker import evaluate, Error_14 def list(field_name, variable_name='$list'): r = []; n = unicode(field_name); i = 1 while True: try: r.append(evaluate("GetNthRecord(%s; %d)" % (n, i))) except Error_14: # out of range break i += 1 return len(r), variable_name, r
Try to call the function with some field name from the PyFM.fmp12
file:
To list an arbitrary field we evaluate FileMaker expressions that calls the
GetNthRecord
function with the passed field name and incrementing record
number. We do not know how many records are there, but we don't need to,
because when you supply the record number that is out of range, FileMaker will
return an error code. This is hard to observe in FileMaker itself, but in the
plug-in API this is explicit. The error code we'll get is #14, “Out of range.”
The first line imports the evaluate
function and Error_14
:
from filemaker import evaluate, Error_14
The filemaker
module dresses all documented FileMaker error codes as
Python exceptions with generic names like Error_14
. If you feel like it,
you can use a more readable name, e.g.:
from filemaker import Error_14 as OutOfRange
The next two lines define the list
function with a single field_name
parameter and prepare the loop:
def list(field_name, variable_name='$list'): r = []; n = unicode(field_name); i = 1
Here the r
is a variable to store the results, n
is the field name as
Python unicode
(it will make it faster to compose the expression), and
i
is the starting record number.
I use one-letter variable names solely for compactness.
Then comes the loop. It's set to run until it's interrupted from inside with
the break
statement. The loop consist of the try/except
statement; if
the contents of the try
block raise the specified exception
(Error_14
), then the except
block will handle it and, in our case,
terminate the loop. If there were no error, we'll get to the last line that
increments the record number i
.
The try
block contains a straightforward instruction to compose an
expression out of name
and i
, evaluate it, and append the result to
the r
variable.
while True: try: r.append(evaluate("GetNthRecord(%s; %d)" % (n, i))) except Error_14: # out of range break i += 1
Finally the last line returns the number of records in r
, the name of
the variable ($list
), and r
itself:
return len(r), '$list', r
When you return more than one result from your function, PyFM treats the first result as the direct result, and the rest as a list of specifications to set variables. You can use the following specifications:
name, value name, (value, value, value, ...) (name, repetition), value
The sample uses tuples, but you can use lists as well. You can mix the specifications in any order, so, for example, the following is a valid instruction:
return 1, '$a', 2, ('$b', 3), 4, '$c', [5, 6, 7]
Here the direct result is 1
, then $a
is set to 2
, $b[3]
to
4
, and $c[1]
, $c[2]
, $c[3]
to 5
, 6
, 7
respectively.
In our case we'll set as many repetititons of $list
as there are values
in r
and also return the number of values, so we know how many we've got.
Now if everything works OK, let's add your files to the plug-in. To do this
open the PyFM.fmp12
file, select the base version and press the New
modification
button to create a new modification:
Enter the modifcation label, for example, MyExt 1.0
and enter the path to
the directory with your files. (You can type the path, copy and paste it, or
choose the directory using the Select
button.) The application will scan
the directory and display its contents.
By default the application selects all files; if you have files or directories you don't want to add just yet, you can deselect them. Also, if you want to only add a file for a certain platform, you can select only that platform.
To add the files to the plug-in press the Modify plug-ins
button. This
step usually takes some time; once it's ready, you'll see the modified
plug-in files in the list of plug-ins.
To install the modified version use the same procedure as with the regular
version: select the target system (current or server) and press the
Install
button.
In the modified version the myext
module is a part of the library, so you
can call it with PyRun:
PyFM plug-ins have a Zip file attached to the plug-in file; the Zip file stores the library of Python modules and other files. When you update a plug-in, the code inside PyFM gets fresh copies of base plug-in files, accesses the Zip archives, and adds your files to them.
If you modify the files or add new ones, rescan the directory and repeat the procedure. Each time you modify the plug-ins the application uses fresh copies of base plug-ins and discards the old modified versions.
The library already has Python modules in it (a good part of standard Python library and some third-party modules); if the path to your file matches one of existing files, the application will skip it. (As of now there's no warning about it.)