Glyphs 3 is here, and Python 2 is on its way out. This tutorial is about getting the code for your scripts and plug-ins to work in both Glyphs 2 and 3, and in both Python 2 and 3. This way, you can ensure a smooth transition for your users.
OK, so this is about upgrading your code in such a way that it will work in both environments. Yes, it can be done. And it is pretty cool, and requires less effort than you may think. We are going to do it in two steps: first we are going to transfer all existing code to Python 3, and in a later step, we will adapt it to the new API.
So, let’s make the changes in our code. Read on.
First, we will stay in Glyphs 2, but upgrade our code to Python 3 and at the same time, keep it compatible with Python 2. To achieve this, you need to do these couple of things in all of your scripts:
- Make sure the heads of your scripts contain these
#MenuTitle: SCRIPTNAME # -*- coding: utf-8 -*- from __future__ import division, print_function, unicode_literals
Change all your
print xstatements into
futurizescript can help you with that, see further below.)
Explicitly import all
Foundation(basic stuff) or
AppKit(anything UI-related). When in doubt, import from
from Foundation import NSPoint, etc.
Now, if you want to keep the script working in Glyphs 2, verify if your scripts still work in Glyphs 2.
Getting Python 3
Check to see if you have Python 3 installed already. In Terminal.app, type:
… and press Return. You should get something like this as a result:
If that is what you have, you’re good. Please skip to the next chapter.
If, however, you get a
command not found error in return, then you need to install Python 3. Te best way to do this is to make sure you have a good internet connection and type this in Terminal.app:
brew install python3
Should you get permission errors, try again with a prepended
sudo brew install python3
If you have to resort to
sudo, you will be asked to type your Mac password. Don't be alarmed if you do not see password bullets, that is just the way Terminal handles passwords. Just type your password ‘blindly’ and press the Return key to continue.
If the error you receive is about
brew being unknown, you may need to install Homebrew first.
Once this is done, try the
python3 --version command again.
Still getting an error? If you know how to handle homebrew, try
brew install python3 or
sudo brew install python3, and then try again. If you still get an error, please make yourself heard in the forum. We will help you there.
Update 2020-01-21: Tim Ahrens reports difficulties under certain conditions: When you run
python3 --version you will receive a command not found error. However, when you do a
brew install python3, it will tell you that no formulae were changed, perhaps adding that Python 3 is already installed but not linked. It will suggest to use
brew link python. If that works for you, fine. You may however still run into trouble, especially symlink error messages. In that case, try
brew link --overwrite python to force the symlink, or delete the offending directory and try
brew link python again.
Again, verify with
python3 --version if everything is done now.
All modern code editors (Sublime Text, TextMate, Atom, but also BBEdit) support finding and replacing across multiple files, or iterating through a folder of .py files. Take a closer look at the options in you Find dialog of your preferred editor. It’s easy.
This, by far, is going to be the biggest change. In fact, for the very most scripts, it will be the only significant change in the code.
In Python 2,
print() is a function. That means it gets those parentheses at the end, and whatever expression you want it to evaluate and print, must be an argument inside those parentheses. In other words, what used to be
print "hello" has now become
print("hello"). Apart from that, it works pretty much the same way it used to do. That includes formatting strings like this:
print("Error in glyph %s."%glyphname), or chaining arguments like this:
You can easily make your code compatible with both Python 2 and 3 by simply adding this import at the top, even before the
from __future__ import print_function
… and then converting every instance of
print "..." into
One special case in Python 2 was the print statement with a comma at the end of the line. That way, the inherent newline at the end of each statement execution was suppressed. In order to replicate this in Python 3, you would have to write:
But that’s about it. So, add that import at the very top, add those parentheses to your
This is probably a non-issue. But there may be an edge case where this may make a difference. In Python 2, strings were 7-bit ASCII by default. In Python 3, they are UTF-8 by default. If you want to start coding this way, and still stay compatible with Python 2, you can add this import at the top of the
from __future__ import unicode_literals
And all your strings will be UTF-8 strings. But do not worry too much about this, because the
u"..." construction still is accepted in Python 3. However, if you like, you can start cleaning out these superfluous u’s now.
One thing you may still need to clean up are
str() calls because they are more likely to throw errors at you. Get rid of them wherever possible and replace them with formatting strings. So, instead of
str(myNum), better use a construction like
Or you don’t care and leave the strings as they are, and only add those imports if you get ‘Non-ASCII’ errors.
In Python 2, if all involved numbers were
int, the whole calculation would be
int only. Typically this would make for surprising results with the division, where
3/2 would yield
1 and not
1.5. In other words, it would default to a floor division.
This is different in Python 3.
1.5. If, for whatever reason, you still need a floor division, you can still use the double slash:
3//2, and get
1 as a result.
I recommend you switch to the Python 3 behavior and always import the new division operator:
from __future__ import division
And feel free to get rid of any
float() conversions you used to throw at your integers in order to prevent the floor division in Python 2.
try statement lets you catch errors, and if one actually happens, it will execute whatever you write after the following
except. However, if you used this construction in Python 2:
except Exception, e:
… you will now have to turn it into this:
except Exception as e:
Note the word
as instead of the comma.
There is a script for updating your
except statements! You should already have it, but if not, you can
pip install future (or
sudo pip install future if you receive an error) in your Terminal, and then try this line inside a folder with
futurize -1 -w *.py
And it will make all the changes above inside all
.py files, and save them back into their files. Ta-daa!
Hint 1: The futurize script also creates
.bakcopies of the
.pyfiles. You may want to keep them around for a short while, just in case. But you definitely do not want those backup files in your git repository. So make sure you include
Hint 2: The script may also insert the
from builtins import strline, which may cause problems if the user does not have
pip. So you may want to (batch) remove that line again. See above.
Glyphs 3 imports
One major change in Glyphs 3 is the fact that
AppKit are not imported automatically at the first script run anymore. Why? Because it significantly slows down the first script the user runs in a session.
It is much better to import only the stuff you need, e.g., like this:
from AppKit import NSPoint, NSRect
That way only the submodules are loaded that the script needs, and the first script runs much quicker. And the user is happy. And that is what we all want, isn't it.
So, what you have to do is go through your code and look for any reference to objects that start with
NS. That include some basic things like
NSRect, and the like. But also more advanced stuff like
NSStringPboardType for clipboard handling.
The good news: I updated the Python for Glyphs snippets accordingly already. Each snippet includes the imports it needs, and where applicable, even the
__future__ imports for Python 2/3 compatibility.
Hint: In Python 3 it is pretty safe (and fast) to use
from Foundation import *. However, this will leave behind the people who use Glyphs 3 with Python 2. So better always import exactly what you need. That is considered better practice anyway.
Have you written plug-ins for Glyphs? You may also need to update their code. But don't worry, it is not much you have to do.
If you (a) have added self-baked method of your own to the plug-in class, and (b) that method’s name does not adhere to the PyObjC-style underscore and camelcase structure, like
doStuffWithArg_andWithArg_(self, A, B), then you need to prepend a python-method decorator. This looks like this:
@objc.python_method def updateView(self, view=None): if view: view.update() return True
If you do not add
@objc.python_method, a selector object will be created for the method. That is not necessarily a bad thing, but you would only want that if you want to access the method from outside the plug-in. And you would have to make it Objective-C compatible by adding underscores for the arguments and using camelcase. Otherwise you will get a lot of errors thrown at you. Or, in the words of the PyObjC documentation:
This is used to add “normal” python methods to a class that’s inheriting from a Cocoa class and makes it possible to use normal Python idioms in the part of the class that does not have to interact with the Objective-C world.
If you already have a decorator in front of the method (rare case), then add the
@objc.python_method even before that other decorator, e.g.
@objc.python_method @property def updateView(self, view=None): if view: view.update() return True
But again, this is a pretty unlikely scenario within the realm of Glyphs plug-ins, so chances are this does not affect you.
If you have a plug-in in the Plugin Manager, you may want to check out the updated SDK. It is now Python 3 compatible. And the templates have an updated ‘MacOS’ folder:
Updated 2020-04-18: In other words, if your plug-in is not exactly recent, make sure the
plugin binary is updated to the current version. Update your
Info.plist with the ones in the SDK, and throw out keys that are not necessary anymore.
Step-by-step guide for upgrading your plug-in
- Remove these files from your plug-in if they are still there (hint: you can do this with the context menu inside the file navigators of TextMate or SublimeText):
- Replace the binary /Content/MacOS/plugin with the corresponding file from the current SDK template. Again, drag&drop with the Opt key works in and between file navigators as well.
- Update your /Content/Info.plist to the structure of the current templates in the SDK. Best to keep them side by side so you see differences right away:
- Make sure your header has:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
CFBundlePackageTypeif they are there. Do the same for
CFBundleSignatureif you still have those hanging around.
- The key
PyMainFileNamesshould have an
<array><string>plugin.py</string></array>and nothing else. Old versions pointed to the main.py file.
- Delete everything after the
PyMainFileNamesarray. Only the closing tags
- Consider updating
- Compare your
<key></key>elements with the ones in the SDK template. Consider throwing out keys that are not in the template. You only need these:
- Make sure your header has:
- Update your /Resources/plugin.py:
- Right after the
# encoding: utf-8line, add the future imports on the second line:
from __future__ import division, print_function, unicode_literals. Hint: the
fut⇥snippet from the Python for Glyphs repository helps.
- After that, make sure you have these imports:
from GlyphsApp import *
from GlyphsApp.plugins import *
- …and whatever other imports you need for your code, I often see
- Get rid of unnecessary legacy imports. In old templates, we used to have
osimports. We do not need those anymore. If you are not sure, comment them out and see if the plug-in works without them.
- If you only need one or two functions from a library, consider the
from ... import ...style of importing, e.g.,
from math import tan, hypotand change any occurrences of
- Decorate all functions (inside and outside your class definition) that do not have a PyObjC-compatible name (=starting with lowercase, camel-cased, underscore for each argument) with
@objc.python_method(this is called the decorator, Google pyobjc decorator it if you are unfamiliar with the concept). I.e., simply put the decorator on the line before the
defstatement, with the same indent as the
defstatement. Hint: the
dec⇥snippet from the Python for Glyphs repository helps.
- Exception to the above: Functions called by a menu item or by a callback must be PyObjC-compatible, i.e., starting with lowercase, camel-cased, and contain an underscore for each argument, and they must have no decorator.
- Turn all
- Turn all
layer.shapes.append(), consider a try/except statement. Hint: the
g23⇥snippet from the Python for Glyphs repository helps.
if x in customParameters:
- That should cover 99% of the changes you will make. For more details on these and other Python 3 changes, read on below.
- Right after the
- Test your plug-in: Force restart Glyphs 3, and put the bundle name of your plug-in into the search field of Console.app, and see what happens.
!PrincipalClassis the most frequent error. If it occurs:
- Make sure
NSPrincipalClassin your Info.plist matches the class name in plugin.py.
- Make sure all necessary decorators are in place.
- Make sure all functions called by menu items and callbacks are undecorated and have a PyObjC-compatible name.
- Make sure you really replaced the /Contents/MacOS/plugin binary with the current one.
- Make sure
Good luck. Report back in the forum if you still have trouble, best in the Upgrading Plug-ins thread.
Major API changes
Then, there is another big thing coming at you. Due to the changes under the hood, the scripting API has changed as well. For the most part, we could keep it the same, but in some areas, the differences are huge.
- Font Info: The way it is going to be organized in Glyphs 3 is going to be very different from Glyphs 2, not in the least to accommodate changes for facilitating variable font production.
- Shapes on the layer: In order to access anything on the layer, including components and paths, you will find yourself looping through
GSLayer.shapesin the future.
Anything visible on a layer is now collected in
GSLayer.components are still around. But they are now merely wrapped shortcuts, and equivalent to
[s for s in GSLayer.shapes if type(s)==GSPath] (and
GSComponent, respectively… you get the idea). That means that this still works:
for path in Layer.paths: print(path) for node in path.nodes: print(node)
But there is one thing you cannot do anymore: delete paths or components by means of their index number. In Glyphs 3,
del Layer.paths will raise a
TypeError: 'GSProxyShapes' object doesn't support item deletion. That is because only the
shapes are properly enumerated now. Here is a sample snippet that shows how you would would recursively delete paths with the
del statement now:
# Delete all paths with less than 4 nodes: for i in range(len(Layer.shapes)-1,-1,-1): shape = Layer.shapes[i] if type(shape) == GSPath: # NEW: check for paths if len(shape.nodes) < 4: del Layer.shapes[i]
Just in case you have never done that: You see the
range(...-1, -1, -1) construction? This gives us all the index numbers in reverse order. That way, when we actually delete an item, the index number of the following shape in the loop does not change.
The other thing you cannot do anymore, is to append a new
GSComponent object to
GSLayer.components, or a
GSPath object to
GSLayer.paths. You append to
GSLayer.shapes instead, like this:
newComp = GSComponent("A") try: # GLYPHS 3: Layer.shapes.append(newComp) except: # GLYPHS 2: Layer.paths.append(newComp)
shapes restructuring means that you can do things like this now: iterate over
shapes, and sort them out by
type with appropriate
if statements. That is, check if the shape is a
GSPath or a
GSComponent, and proceed accordingly. E.g., this is how you would loop over all shapes on a layer:
for i, shape in enumerate(Layer.shapes): print(i, shape) if type(shape) == GSPath: for node in shape.nodes: print(node) elif type(shape) == GSComponent: print(shape.position)
Similarly, if you want to delete all components but at the same time keep all paths that may be on a layer, you may have been used to do something like
Layer.components=None. But that won’t work anymore. So you will have to iterate backwards through the enumerated
del the ones that check out as
for i in range(len(Layer.shapes)-1, -1, -1): if type(Layer.shapes[i]) == GSComponent: del Layer.shapes[i]
Likewise if you want to delete all
GSPath objects and keep
GSComponent objects. Just test for
GSPath rather than
GSComponent, of course.
This will be one of the biggest changes in your scripts and plug-ins.
Guides are called guides now. In other words, there’s no more ‘Line’ at the end. That means:
GSGuide is the new class name, and you would access
GSFontMaster.guides. Other than that, everything is the same.
Supporting both Glyphs 2 and 3
Now, in your code transition to Glyphs 3, you may want to also support app version 2. And luckily, there is a way to do it without having to resort to maintaining two repositories. It is easy: check for
Glyphs.versionNumber. Here is one of the examples from above:
if Glyphs.versionNumber >= 3:: # GLYPHS 3: for shape in Layer.shapes: if type(shape) == GSPath: print(shape) for node in shape.nodes: print(node) else: # GLYPHS 2: for path in Layer.paths: print(path) for node in path.nodes: print(node)
It is that easy, works like a charm in both version two and three.
Further reading? Here you go:
- Glyphs API
- Python Future, especially its Cheatsheet for writing 2 & 3 compatible code
- python.org: Porting Python 2 Code to Python 3
- PyObjC bridge documentation
- pip installation and quickstart
- Python for Glyphs snippets for TextMate and SublimeText
- The Dev category in the forum: if you do not have access, ask @mekkablue for it.
- Get feedback from your users in the Beta Test category in the forum, especially in the Plug-ins thread.
Update 2022-08-07: if…else instead of try…except for G2+G3 code. Removed some outdated code samples.