A type annotated wrapper around DrawBot
-
First of all, why? Don't we already have DrawBot?
During the last year, I started to study and use Swift. It's not so hard to learn if you come from Python, the syntax is quite similar, but you have to get used to handling types. That's a bit annoying at the beginning, but it has its advantages. In a way, it made me realize that I often write crappy Python code (unwrapping optionals? what's that?).
Handling types is frustrating because you get all these annoying error messages from your IDE, like "this function might return a
None
value, are you prepared for that?". It slows you down and it forces you to cast ints as floats for asin()
function and other things only meaningful to Computer Science people. At the same time, all these annoying messages risk becoming errors at runtime. And it sucks if you are going to distribute the code to other users. Bugs mean extra complaints and less confidence. Bad for users and developers.How can I get some extra confidence in writing my code while using Python? I like Swift, but Python is useful, especially for font development work. Maybe I can get a similar experience to writing Swift by adding type annotations to Python code. But, you can't check annotations where they do not exist. And most of the packages I use for my work do not have annotations. So, I screamed to the sky: «I will add annotations to DrawBot!»
Well, that's not so easy. Let's start with a wrapper around DrawBot, then we'll see.
-
What is DrawBot?
I usually present DrawBot to non-type people in this way: "It is a cousin of Processing, but: (1) you write Python code, (2) it handles text and vectors super well with powerful APIs like
BezierPath
andFormattedString
, (3) it's not meant for real-time interaction". Quoting from the DrawBot home page:DrawBot is a powerful, free application for macOS that invites you to write simple python scripts to generate two-dimensional graphics.
DrawBot is © by Erik van Blokland, Just van Rossum, Frederik Berlaen
-
Why a wrapper?
Consider that the DrawBot API makes extensive use of duck typing in the way it structures its API. For example, when you pass a coordinate to a function like
text(txt, (x, y), align=None)
, DrawBot does not care if you pass atuple
, alist
, anamedtuple
, a class acting like a sequence (like a customVector
class). DrawBot says «Two numbers? Cool, I need nothing more, let's draw». That's very cool and flexible, but flexibility sometimes can lead to bugs. Annotating this kind of API might be difficult and it can generate ugly code. And I don't want my contribution to DrawBot to be remembered for being ugly 🥲. -
Wait, did you say Duck Typing? Like the animal?
"If it walks like a duck and it quacks like a duck, then it must be a duck" see 1 and 2
-
Type annotations are an interesting topic, I want to know more.
You can start reading a few PEPs related to type annotatios:
I have also found these talks useful:
-
If type annotations are not checked at runtime, how do I know if I am doing something wrong?
You need to use a static type checker, like mypy or pyre. You could tie the static type checker to your favorite code editor linter functionality. I use SublimeText with the SublimeLinter plugin. You can install mypy as extension for the SublimeLinter plugin. In my experience, in a similar way to unit tests, type annotations are extremely helpful during the refactoring process.
-
Ok, sold. I want to try it. How can I install it?
pip install git+https://github.com/roberto-arista/TypedBot
Be aware, it's the first package I release, so I probably did something wrong in
setup.py
or similar. Open an issue if something goes wrong and I'll try to figure out a solution. -
Wait, is this stable? Can I use it in production?
Yes, no, almost. DrawBot is stable and well tested. TypedBot, not yet. Consider that:
- part of the DrawBot API is not annotated yet (see
imageObject
) - I am using this module in a couple of projects and the experience acquired will probably push me to adjust or change some things
IMHO you can use it, but you need to be a bit flexible
- part of the DrawBot API is not annotated yet (see
-
How do I know if TypeBot draws in the same way DrawBot does?
TypedBot tests suite revolves around this issue. I ported a selection of scripts coming from the DrawBot tests suite, made a "translation" to TypedBot, and compared the results pixel by pixel. If you think that some functionality is not properly tested yet, let me know with an issue
-
Is the TypedBot API equivalent to the DrawBot API?
Not completely. Type annotations syntax allows to annotate functions and methods of any kind in Python, but the result is not always great code. Considering that this wrapper is separate from DrawBot and that it does not break any existing functionality, I decided to take some liberty
-
Ok, so how do I know what differs between the two APIs?
The source code of TypedBot and the DrawBot docs are the best references. Here are some principles I followed (hopefully consistently across the API):
- I tried to avoid optionals as much as possible. In other words, I tried to avoid letting parameters accept
None
as a value. I know thatfill(None)
is an idiomatic way of sayingfill(0, 0, 0, 0)
, but in this framework, I'd rather be strict and avoidNone
- In the DrawBot API functions often accept sequences of numbers for colors, points, or boxes. For example:
Annotating this function would result in something like:
fill(1, 0, 0) text('hello', (100, 100))
Which is not optimal in my opinion. So I decided to create a few dataclasses likedef fill(clr: Optional[Tuple[float, float, float]]): pass def text(txt: str, pt: Tuple[float, float]): pass
Color
,CMYKColor
,Box
andPoint
. So the annotations become:def fill(clr: Color): pass def text(txt: str, pt: Point): pass
- I decided to extract some functionalities from
newPage
,saveImage
,fontVariations
andopenTypeFeatures
and to direct it to other functions.newPage
accepts two floats for width and height andnewPageDefault
accepts a string value for a default page size, like "A4". Making aUnion[float, str]
for the first argument of the function did not make much sense IMHOsaveImage
is a very dense and cool method in DrawBot, but it is very difficult to annotate clearly! It has a ton of default keywork arguments, and I decided to make a clone for each format (png, jpg, pdf...)fontVariations
andopenTypeFeatures
mix*args
and**kwargs
in order to be able to haveresetVariations
orresetFeatures
boolean argument. It felt natural to extract this functionality into separate functionsresetVariations()
andresetFeatures()
- I tried to avoid optionals as much as possible. In other words, I tried to avoid letting parameters accept
-
This project is cool, I want to help you. How can I contribute to it?
That's great! Right now I see three areas where I could need some help:
- It would be great to increase the test suite and compare even more scripts between drawBot and typedBot
- It would be nice to have some docs online. I don't know what these docs should show, probably each method should have a link to the drawBot's original version and the annotations.
- Part of the API is not annotated yet. For example the
imageObject
. It has a large API (about 100 methods?) and I seldom use it. So, it's not my priority to annotate it.
If you want to contribute, I suggest to:
- fork the repo
- clone it on your machine
- install it locally in editable mode with pip
cd path/to/cloned/repo pip install -e .
- make a pull request!
-
I like the general sense of TypedBot, but I do not like SOME features
TypedBot is also an opportunity to sparkle some (healthy I hope) discussion in the type-tech-tool world about annotations. Do we want to use them? Should we make an effort to annotate existing codebases? Maybe not, I am not sure. So, if you do not agree with my choices, that's fine. Open an issue, maybe submit a pull request with your ideas, and let's discuss! I also invite you to try it in your projects, if you think the wrapping should happen differently, you don't need to wrap the entire API, just work on the functions you need and give it a spin!
-
What about the license?
TypedBot has the same license as DrawBot, so you can use it wherever you use DrawBot
-
Extra note
I started to develop this project during the Winter #1 2021 batch at Recurse Center. Such a cool place!