Skip to content

Discoverable Python plugin support #4642

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 21 commits into from
Mar 26, 2021
Merged

Discoverable Python plugin support #4642

merged 21 commits into from
Mar 26, 2021

Conversation

skef
Copy link
Contributor

@skef skef commented Feb 24, 2021

This is the deluxe version of the discoverable plugin interface I discuss in #4062. At the bottom level the idea is quite simple -- provide a way for python packages to identify themselves as plugins (the Entry-points mechanism), then import each package and call a prearranged function on its identified object (usually the module itself).

Once this is in place a developer can adapt an "init script" into python package and upload it to PyPI. A user who finds and installs that package can then simply enable it in the "Configure Plugins..." dialog under the File menu (or choose that all new plugins are automatically enabled).

Most of the hoo-hah is UI-related, as usual. It seemed inevitable that users would complain if they couldn't control the initialization order, and therefore the Tools menu order, so I provided that ability. There may also be some cases where security is a worry so a user need not enable new plugins by default and can turn some off. It also seemed good to provide (optional) preferences configuration (yet to be described in the documentation but fairly clear from the code) in a central window rather than cluttering Tools menus with it. Same with the project URLs.

I don't know how much code sharing this will wind up facilitating but it does provide these key advantages:

  1. Rather than building our own database we can just link to https://pypi.org/search/?q=%22FontForge_plugin%22. (I've specified that each plugin should contain the string "FontForge_plugin" in its package metadata description field.)
  2. 'For Python contrib conventions #4062 the project can just maintain a meta-package (with no contents but dependencies on each of the plugins we consider "key" contributions). We can then encourage distros to provide that package in their usual way, and (perhaps?) install it automatically in our embedded pythons (presumably using pip over the web in the normal way). Users can then upgrade those packages as they like also in the normal way, so no code-rot in our repos.

I expect this PR will take some time to review and will change a bit before it is merged, but I think it's more or less ready except for the documentation. Those docs, however, should be developed in part in response to developers' feedback.

I also want to have a discussion about those users. I'm thinking a new fontforge-plugin-dev mailing list would be good (it doesn't seem like that's what fontforge-dev is for), along with the offer (once this actually ships in an official release) to announce significant new plugins on fontforge-announce.

I'll upload a couple silly plugins to play with shortly.

This work has been supported by the Tug libre fonts fund, and also, I guess, by myself, considering that it took longer than I expected. @davelab6 should take a look.

Closes #4581

@lgtm-com
Copy link

lgtm-com bot commented Feb 24, 2021

This pull request introduces 1 alert when merging 503d1f5 into db455c1 - view on LGTM.com

new alerts:

  • 1 for Missing return statement

@skef
Copy link
Contributor Author

skef commented Feb 24, 2021

Here are some sample plugins, both source and installable archives (in dist):
play_plugins.zip

As the name suggests these aren't polished systems but quick adaptions of scripts I had lying around.

@ctrlcctrlv
Copy link
Member

We need more information.

  • Which plugins will be distributed by default?
  • Which libraries are we guaranteeing to exist? Tkinter? Or is the plugin manifest supposed to specify Tkinter?
  • Is there a UI for managing plugins?

@skef
Copy link
Contributor Author

skef commented Feb 25, 2021

There is a UI for configuring how the plugins installed on your own system interact with your instance of FontForge. You bring it up with a menu option under "File". That stuff is described in the documentation added in this PR.

Your other questions relate to the discussion/disagreement in #4062. Note that I'm not marking that issue as being closed by this. This is a mechanism that allows user-developers to more easily share their FontForge extensions with the user base. My suggestion that we address "the contrib question" with a meta-package is just that, the project needn't go that route.

There are a number of improvements to the Python API I plan to pursue in the gap between this being merged and the next release, whenever that is. (This is assuming I have the time to pursue them, of course.) I don't want to get into that "here" because they're not directly related; they would be just as much benefit to an "init script" developer as a plugin developer.

However, one is a limited means of specifying multi-parameter dialogs, similar to the "could we just use the Params dialog template?" idea that @ctrlcctrlv and I discussed a while back. It's a compromise that lets a user bring up a dialog with a lot of different options without a lot of control over how they're spatially presented. I think it will work for 90% of the cases and in a way that's far less likely to rot over time. This will be a bit of work: In addition to just the general UI foo (I think?) I'll need to build a wrapping layout manager analogous to GHVBoxCreate.

(Another improvement is adding an "Apply protocol" callback option to the ask-like dialogs, including the new one, so that you make them work like the current Transform dialog.)

@ctrlcctrlv
Copy link
Member

Oh okay, I understand the scope of this change now. Tkinter can be provided as a requirements.txt entry, so I'm just noting that can happen with Pip installable packages.

If you consider contrib a separate problem, I agree, so we can put a pin in that for now :)

@ctrlcctrlv
Copy link
Member

I'm thinking the best way for me to test this is to convert ctrlcctrlv/QuickAnchors into a pip package. Thoughts on that approach?

@skef
Copy link
Contributor Author

skef commented Feb 26, 2021

Thoughts on that approach?

There are somewhat separate questions of "Is this functionality worth adding?" (testing the idea) and "Does this do what it says it does without crashing?" (testing the code). The first question also closely relates to what documentation we should write and what further steps, if any, we should take with the python dev and user communities.

So what I would say is: That's absolutely a great idea relative to the first question and I strongly encourage it. If your plugin doesn't have or need preferences you might get by with just the techref/pyextend.rst document I've added (and attempting to just get by with it would tell us some ways it needs to be improved). I just looked at the existing script and it has the "typical" structure I describe in that doc.

(I don't yet say anything about dependencies, and the docs probably should. In many cases you might get away with just an install_requires directive in the setup.cfg file (under "[options]" and similar in format to "classifiers" in the "[metadata]" section). Whether you need a requirements.txt file seems to be a complicated question.)

If the plugin does have preferences or you want to add them, well, I should add something about the preferences callback anyway -- it's quite simple. (However, before there's more development work on the "ask' dialogs you might want to TkInter yourself a window for that.)

Anyway, this could take you as little as 15 minutes or so, if you just copy and adapt the current MidContour config into your https://github.com/ctrlcctrlv/QuickAnchors repo. (I would skip PyPI for now, both because it's more work and because people may get confused if they start seeing plugins listed before a released version of FontForge can support them.)

For breadth of testing, which I've already done some of, we can mix your plugin with some of the fake ones I uploaded and folks can play around with them. So: a benefit but not a solution.

@skef
Copy link
Contributor Author

skef commented Feb 26, 2021

Oh -- @jtanx I forgot a lingering issue in this code.

If you were to glance at pluginui.c:FigurePluginList() in this PR you'd notice:

    GList_Glib *p;


    for (i = 0, p = plugin_data; p != NULL; ++i, p = p->next) {
        PluginEntry *pe = (PluginEntry *) p->data;
        pe->new_mode = pe->startup_mode;
        ti[i] = calloc(1, sizeof(GTextInfo));
        ti[i]->text = pluginDescString(pe, &has_err);
        ti[i]->fg = ti[i]->bg = COLOR_DEFAULT;
        if (has_err) {
            ti[i]->fg = COLOR_CREATE(0xBD, 0x43, 0x37);
        }
        ti[i]->userdata = pe;
    }

One thing you may be happy about is that plugin_data is a GList_Glib pointer. One thing you may be less happy about is that hard-coded color.

I looked in our X Resources stuff and:

  1. It doesn't look like there's a standard "error condition" color, nor is there a (gdraw) GList-specific error condition color.
  2. It wasn't clear to me how I would go about adding one, given the depth and breadth of all that stuff.

Do you have any thoughts on this? Best practices? Implementation help? I can also just abandon the idea ...

@jtanx
Copy link
Contributor

jtanx commented Feb 27, 2021

Do you have any thoughts on this? Best practices? Implementation help? I can also just abandon the idea ...

Your guess is as good as mine to be honest. You could probably add the concept in if you spent enough time, but I'm not sure it's worth the effort. I'd be fine for hard-coding it for now, even if it is perhaps not the most ideal.

Which libraries are we guaranteeing to exist? Tkinter? Or is the plugin manifest supposed to specify Tkinter?

This is purely a packaging decision, like your Python distribution either comes installed with tkinter or it doesn't, you can't pip install it. I am OK with that, I don't think the plugin system should prescribe whether or not it's required. If your plugin depends on something and it isn't available, too bad (of course since these are basically just python packages, if they can install dependencies via pip then fine). We have control over the binaries we ship, and can choose to include or exclude whatever we like, but that doesn't prevent distros/users from doing what they like, and if they choose to install python without tkinter then so be it.

@skef
Copy link
Contributor Author

skef commented Feb 27, 2021

If your plugin depends on something and it isn't available, too bad

The only thing I would add to this is that it would probably be a good idea to define (or, more likely, pick) an exception type that, when returned from fontforge_plugin_init(), indicates that the plugin can't find a runtime that can't be handled via dependencies. Then I can give that a separate "Info String" to display in the dialog. I'll look into that.

@davelab6
Copy link
Member

davelab6 commented Feb 28, 2021 via email

@skef
Copy link
Contributor Author

skef commented Feb 28, 2021

@davelab6 It's not so much about the current amount of traffic as the potential burst in traffic and who may be subscribed. If folks signed up thinking the list was for direct contributions and not helping people with their plugins and sorting out the API then they could get annoyed. But it could also be fine, I don't really know.

@skef
Copy link
Contributor Author

skef commented Feb 28, 2021

I just checked the archives and traffic has been at such a low level that we can probably just send a note warning about the change in emphasis on fontforge-devel so people can unsubscribe if they want. Seems easiest.

@ctrlcctrlv
Copy link
Member

tkinter can be added to setup.py install_requires like any other package , but if it isn't found it can't be installed without manual intervention, it won't be automatically pulled from pip because it's not pip-installable. Plenty of packages still put it in install_requires though.

This isn't really our problem because pip will refuse to install the package way before FontForge ever crashes on it, so it's more of a contrib issue than anything.

@skef
Copy link
Contributor Author

skef commented Mar 1, 2021

I added a warning for enabled plugins that aren't found and significantly expanded the documentation.

@frank-trampe
Copy link
Contributor

Back when I got subscription change notifications, we did indeed get a wave of cancellations whenever somebody hijacked the announcement list for a user or engineering issue. I imagine that the same holds true for the user list. If something is beyond the understanding or interest of a casual user, I think that we need to keep it on the engineering list.

@skef
Copy link
Contributor Author

skef commented Mar 2, 2021

@frank-trampe: So maybe one announcement on fontforge-users noting that there's going to be a phase of discussion about contributed features on fontforge-dev. One warning on fontforge-dev in case anyone wants to unsubscribe in the fact of increased traffic?

@frank-trampe
Copy link
Contributor

That sounds generally right to me, although I'm inclined to push as much of the substantive engineering discussion to the issue tracker. I propose the following. The users list gets a notification that we are working on a new plug-in interface, with discussion happening here. The engineering list gets the same notification with the additional note that discussion of new plug-ins may happen on the engineering list and that we will announce significant plug-ins (likely in conjunction with mainstream releases) on the announcement list.

@@ -370,7 +372,7 @@ static bool DiscoverPlugins(int do_import) {
Py_DECREF(tmp);
if (PyIter_Check(tmp2)) {
while ((str = PyIter_Next(tmp2))) {
tmp = PyUnicode_AsASCIIString(str);
tmp = PyUnicode_AsUTF8String(str);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You should be able to use PyUnicode_AsUTF8 and skip having to manage temporary python objects/freeing anything yourself

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

pe->summary);
PyList_Append(r, t);
entry = PyDict_New();
tmp = PyUnicode_FromString(pe->name);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think you need to Py_DECREF these. Might be easier to still use Py_BuildValue? e.g.

Py_BuildValue("{s:i,s:i}",
              "abc", 123, "def", 456)    {'abc': 123, 'def': 456}

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK ... done

@jtanx
Copy link
Contributor

jtanx commented Mar 13, 2021

Was just playing around with this on Windows - had a couple of questions/notes

  • What's the point of the delete button if all it does is disable the plugin?
  • When you press delete then cancel, the state changes (the plugin becomes disabled), but I would have expected it to cancel the action and do nothing. Similarly it moves it to the bottom of the list(?)
  • What does it mean to load a plugin without enabling it? Is this like a once-off load for this session that isn't persisted?
  • Not that big of a deal, but there's no way to select multiple to enable/disable
  • There are a lot of options presented on the plugin window - a little overwhelming to be honest.
  • When I click enable for a plugin, I would expect it to be loaded immediately

@skef
Copy link
Contributor Author

skef commented Mar 14, 2021

What's the point of the delete button if all it does is disable the plugin?

Delete is primarily for removing the entry of a plugin that is no longer installed. I suppose I could just enable it in that case -- there's no particular reason a user should be able to put an entry back in "new/ask" mode.

When you press delete then cancel, the state changes (the plugin becomes disabled), but I would have expected it to cancel the action and do nothing. Similarly it moves it to the bottom of the list(?)

I'll look at that.

What does it mean to load a plugin without enabling it? Is this like a once-off load for this session that isn't persisted?

Yes.

Not that big of a deal, but there's no way to select multiple to enable/disable

Yeah, I'm probably not going to get into that for this release.

There are a lot of options presented on the plugin window - a little overwhelming to be honest.

It's the usual problem that 80% of the stuff will only be wanted by 20% of the users, but the latter will want those things.

When I click enable for a plugin, I would expect it to be loaded immediately

I'll think about that. People who don't care about ordering will expect that. People who do care about ordering might not.

Offer to load newly enabled plugins
@skef
Copy link
Contributor Author

skef commented Mar 20, 2021

I updated the code so that only an undiscovered plugin can be deleted.

I also now offer (in a button choice dialog) to load a plugin that is newly enabled.

When you press delete then cancel, the state changes (the plugin becomes disabled), but I would have expected it to cancel the action and do nothing. Similarly it moves it to the bottom of the list(?)

This is likely no longer relevant but I'm not sure how it would have been happening. If you still see something along these lines please describe a repeatable test case.

@jtanx
Copy link
Contributor

jtanx commented Mar 20, 2021

This is likely no longer relevant but I'm not sure how it would have been happening. If you still see something along these lines please describe a repeatable test case.

Since you removed the ability to 'Delete' a discoverable plugin (so you can't cancel the action) it's no longer possible.

@jtanx
Copy link
Contributor

jtanx commented Mar 20, 2021

What are your views on init scripts going forward? Keep it? Deprecate it? Let me know if I'm missing anything, but I see it as the functionality being basically the same as an init script except you add on better discoverability(assuming people follow the conventions when uploading to pypi)+version management and that you've added a mechanism to load them from system python.

@skef
Copy link
Contributor Author

skef commented Mar 20, 2021

If someone is just making a little extension for themselves and doesn't want to get into Python packaging an init script still seems like a fine mechanism. It's a bit unfortunately hidden at this point -- the location where they need to be isn't documented very well/accurately. But I still expect future plugins will start their lives as init scripts.

I guess my view is that init scripts will continue to be for "personal use" and plugins will be for when someone generalizes a feature enough to make it worth sharing. The biggest advantage of plugins is PyPI and, once things get going, the https://pypi.org/search/?q=%22FontForge_plugin%22 link. Rather than building its own database, distribution, and installation system the FontForge project can just use Python's. That's the main thing this buys.

Copy link
Contributor

@jtanx jtanx left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not a comprehensive review by any means, but I think some things you just find easier to discover/fix once it's out there.

Seems like a pretty decent change, nice work.

return pe;
}

static char *getPluginDirName() {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My preference would be to at least keep the casing consistent within a file, especially new files; with pascal case for functions unless replacing stuff that's already following some other pattern.

Using a plugin is as easy as installing the package (often with
`pip <https:pypi.org/project/pip/>`_) and, when necessary, "configuring"
the plugin. Almost any plugin of general interest will be listed in
the `PyPI package database <https:pypi.org/search/?q=%22FontForge_plugin%22>`_.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's sad that there isn't proper support for custom tags (like you just have to have it as part of the description), but oh well, that's a pip/pypi limitation.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I switched the new functions to Pascal case.


First we create a new directory for our project with the following files:

1. ``fontforge_midpoint.py`` (The script itself as a single module file.)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One thing you can do is to create something like a cookiecutter template that allows you to quickly bootstrap a python package. It's what I use at work, and it's pretty handy in saving you from doing a lot of the boilerplate.

@skef
Copy link
Contributor Author

skef commented Mar 21, 2021

Any remaining thoughts before this is merged? @frank-trampe?

This would be my initial guidance on "communication", which it might be better to delay kicking off until #4671 and maybe the registerMenuItem() improvements are merged:

  1. We announce to fontforge-users that the upcoming release will have a number of UI extension improvements and a Python plugin system, so that extensions can be bundled and installed as normal Python packages (searchable on PyPI, installable with pip). Anyone who may be interested in extension development should subscribe to fontforge-dev.
  2. A bit later we announce to fontforge-dev that there may be increased traffic due to discussion of the new extension features. We stress that we very much want to get the solutions sitting in individual init scripts more generally available. Then we encourage the following:
    • Interested users should consult the new technical documentation and probably an FAQ on the Wiki where we can provide important answers.
    • Users with difficult/specific questions are encouraged to look through the relevant categories at https://github.com/fontforge/fontforge/discussions to see if their question is answered, and if not open a new discussion.
    • Users with more general "how do I do blah" questions can feel free to ask on fontforge-dev (That's what it's for, I think?)
    • Users who make a plugin that may be of interest should contact us (how? email?) so that we can mention it on fontforge-announce.

I don't have discussion category edit privileges. Can someone maybe do https://docs.github.com/en/discussions/managing-discussions-for-your-community/managing-categories-for-discussions-in-your-repository#creating-a-category to add "Plugin Q&A" and "Plugin Show and Tell" categories?

@skef
Copy link
Contributor Author

skef commented Mar 25, 2021

Alright, if there are no updates before Thursday evening I'll merge this.

@skef skef merged commit 0161f5c into fontforge:master Mar 26, 2021
@skef skef mentioned this pull request Jul 1, 2021
@skef skef mentioned this pull request Jan 27, 2022
@tisimst
Copy link

tisimst commented Feb 7, 2022

@skef, This sounds really exciting. Thanks for all your amazing work on this and all the recent improved python support. I don't think I'm the only one who will welcome some improvements in the plugin realm.

I feel quite like a dunce, though, not able to understand how I can use this on Windows or how it differs from "the old way" of creating plugins/scripts. Do you (or someone) plan on putting together a step-by-step tutorial (with pictures, preferably)? That would help me tremendously and I think would spur more plugin development from the userbase.

@skef
Copy link
Contributor Author

skef commented Feb 7, 2022

The windows version of FontForge (at least the one that we compile) has a built-in python interpreter separate from the system one (assuming one is installed). But I believe it bundles pip, which can be used to install PyPI packages.

As far as plugin development goes, the plugin interface itself doesn't make that much different. The multiask dialog and the improved menu support (assuming that gets merged) do improve the options. The plugin idea is more about user discovery and distribution. Sharing FontForge scripts never really took off, partly because there was no master list and "installing" them can be confusing. Now people will be able to search on a keyword in PyPI and install additional FontForge functionality using the python package ecosystem.

We will probably set up a "blank plugin" repository as a starting point for interested developers at some point (@jtanx's suggestion).

@tisimst
Copy link

tisimst commented Feb 7, 2022

I see now. Thanks for the additional explanation.

I have developed quite a library of functions I use regularly through the plugin framework. This could entice me to break out what I've done into separate, shareable plugins. I'd certainly be interested to know if there are many others who have successfully gotten their own plugins to work to enhance FF's functionality because it can be very frustrating at times. It's been a huge blessing for me, though, to have the ones I've created.

@ctrlcctrlv
Copy link
Member

@tisimst I doubt there are many, because this isn't in a release yet, but there is also me. If you meant, “have many people managed to do all sorts of things via FontForge Python API (before it included pip-packaged plugins)?" the answer is certainly yes.

@tisimst
Copy link

tisimst commented Feb 11, 2022

Well, I created all of my plugins well before this functionality manifested. I think you're probably right, though, that there aren't a lot of people who've taken the plunge and gotten very far before throwing their hands up in frustration. I certainly almost did, but was able to develop a few very much needed helper functions that could adequately tell me when I was doing something wrong.

Anyway, still, if the ability to pass plugins along via pip works well, I'd consider figuring out how to package mine to share with others.

Omnikron13 pushed a commit to Omnikron13/fontforge that referenced this pull request May 31, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Unclear how to delete a contour from a glyph’s layer
6 participants