Scripting: upgrading to Python 3
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.
Quick guide
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
__future__
imports:
#MenuTitle: SCRIPTNAME
# -*- coding: utf-8 -*-
from __future__ import division, print_function, unicode_literals
-
Change all your
print x
statements intoprint(x)
functions. (Thefuturize
script can help you with that, see further below.) -
Explicitly import all
NS
objects fromFoundation
(basic stuff) orAppKit
(anything UI-related). When in doubt, import fromAppKit
, becauseAppKit
includesFoundation
. E.g.,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.
In detail
Getting Python 3
Check to see if you have Python 3 installed already. In Terminal.app, type:
python3 --version
… and press Return. You should get something like this as a result:
Python 3.7.5
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
:
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.
Batch replacing
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.
print()
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
was a statement. That means that you would write the word print
followed by a space, followed by whatever expression you wanted to be evaluated and printed. In Python 3, 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: print("a","b","c")
.
You can easily make your code compatible with both Python 2 and 3 by simply adding this import at the top, even before the #MenuTitle
line:
from __future__ import print_function
… and then converting every instance of print "..."
into print("...")
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:
print(letter, end='')
But that’s about it. So, add that import at the very top, add those parentheses to your print
statements, and you’re all set.
Strings
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 .py
file:
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 "%i"%myNum
.
Or you don’t care and leave the strings as they are, and only add those imports if you get ‘Non-ASCII’ errors.
Division
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. 3/2
yields 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.
Exceptions
The 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.
Futurize
There is a script for updating your print
and 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 .py
files:
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
.bak
copies of the.py
files. 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*.bak
in your.gitignore
.
Hint 2: The script may also insert the
from builtins import str
line, which may cause problems if the user does not havefuture
installed frompip
. 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 Foundation
and 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 NSPoint
, NSRect
, and the like. But also more advanced stuff like NSPasteboard
and 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.
ObjectiveC decorators
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. @property
:
@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.
Updated SDK
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):
- /Content/PkgInfo
- /Content/MacOS/main.py
- /Content/MacOS/python
- /Content/Resources/__boot__.py
- /Content/Resources/__error__.sh
- /Content/Resources/site.py
- 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">
- Remove
CFBundleDisplayName
andCFBundlePackageType
if they are there. Do the same forNSMainNibFile
andCFBundleSignature
if you still have those hanging around. - The key
PyMainFileNames
should have an<array><string>plugin.py</string></array>
and nothing else. Old versions pointed to the main.py file. - Delete everything after the
PyMainFileNames
array. Only the closing tags</dict>
and</plist>
should follow. - Consider updating
CFBundleVersion
,CFBundleShortVersionString
,productReleaseNotes
. - 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:CFBundleDevelopmentRegion
,CFBundleExecutable
,CFBundleIdentifier
,CFBundleInfoDictionaryVersion
,CFBundleName
,CFBundleShortVersionString
,CFBundleVersion
,UpdateFeedURL
,productPageURL
,productReleaseNotes
,NSHumanReadableCopyright
,NSPrincipalClass
,PyMainFileNames
.
- Make sure your header has:
- Update your /Resources/plugin.py:
- Right after the
# encoding: utf-8
line, add the future imports on the second line:from __future__ import division, print_function, unicode_literals
. Hint: thefut⇥
snippet from the Python for Glyphs repository helps. - After that, make sure you have these imports:
import objc
from GlyphsApp import *
from GlyphsApp.plugins import *
- …and whatever other imports you need for your code, I often see
math
there. - Get rid of unnecessary legacy imports. In old templates, we used to have
sys
andos
imports. 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, hypot
and change any occurrences ofmath.tan()
ormath.hypot()
to simplytan()
andhypot()
, etc.
- 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 thedef
statement, with the same indent as thedef
statement. Hint: thedec⇥
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
print
statements intoprint()
functions. - Turn all
layer.paths.append()
andlayer.components.append()
intolayer.shapes.append()
, consider a try/except statement. Hint: theg23⇥
snippet from the Python for Glyphs repository helps. - Turn
if customParameters.has_key(x):
intoif 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.
!PrincipalClass
is the most frequent error. If it occurs:- Make sure
NSPrincipalClass
in 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.shapes
in the future.
Shapes: paths and components
Anything visible on a layer is now collected in GSLayer.shapes
. Yet, GSLayer.paths
and 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[2]
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)
The 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 shapes
and del
the ones that check out as GSComponent
:
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.
GSGuide
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 GSLayer.guides
and 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.
Resources
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
- Homebrew
- 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.