Search code examples
pythonmatplotlibsetuptoolsdistutils

How do I distribute fonts with my python package?


I have created a package called clearplot that wraps around matplotlib. I have also created a nice font that I want to distribute with my package. I consulted this section of the Python Packaging User guide, and determined that I should use the data_files keyword. I chose data_files instead of package_data since I need to install the font in a matplotlib directory that is outside of my package.

Here is my first, flawed, attempt at a setup.py file:

from distutils.core import setup
import os, sys
import matplotlib as mpl

#Find where matplotlib stores its True Type fonts
mpl_data_dir = os.path.dirname(mpl.matplotlib_fname())
mpl_ttf_dir = os.path.join(mpl_data_dir, 'fonts', 'ttf')

setup(
    ...(edited for brevity)...
    install_requires = ['matplotlib >= 1.4.0, !=1.4.3', 'numpy >= 1.6'],
    data_files = [
        (mpl_ttf_dir, ['./font_files/TeXGyreHeros-txfonts/TeXGyreHerosTXfonts-Regular.ttf']),
        (mpl_ttf_dir, ['./font_files/TeXGyreHeros-txfonts/TeXGyreHerosTXfonts-Italic.ttf'])]
)

#Try to delete matplotlib's fontList cache
mpl_cache_dir = mpl.get_cachedir()
mpl_cache_dir_ls = os.listdir(mpl_cache_dir)
if 'fontList.cache' in mpl_cache_dir_ls:
    fontList_path = os.path.join(mpl_cache_dir, 'fontList.cache')
    os.remove(fontList_path)

There are two issues with this setup.py:

  1. I attempt to import matplotlib before setup() has a chance to install it. This is an obvious booboo, but I needed to know where mpl_ttf_dir was before I ran setup().
  2. As mentioned here, wheel distributions do not support absolute paths for data_files. I didn't think this would be a problem because I thought I would just use a sdist distribution. (sdists do allow absolute paths.) Then I came to find out that pip 7.0 (and later) converts all packages to wheel distributions, even if the distribution was originally created as a sdist.

I was quite annoyed by issue #2, but, since then, I found out that absolute paths are bad because they do not work with virtualenv. Thus, I am now willing to change my approach, but what do I do?

The only idea I have is to distribute the font as package_data first and then move the font to the proper location afterwards using the os module. Is that a kosher method?


Solution

  • Thanks to @benjaoming's answer and this blog post, here is what I came up with:

    from setuptools import setup
    from setuptools.command.install import install
    import warnings
    
    #Set up the machinery to install custom fonts.  Subclass the setup tools install 
    #class in order to run custom commands during installation.  
    class move_ttf(install):
        def run(self):
            """
            Performs the usual install process and then copies the True Type fonts 
            that come with clearplot into matplotlib's True Type font directory, 
            and deletes the matplotlib fontList.cache 
            """
            #Perform the usual install process
            install.run(self)
            #Try to install custom fonts
            try:
                import os, shutil
                import matplotlib as mpl
                import clearplot as cp
    
                #Find where matplotlib stores its True Type fonts
                mpl_data_dir = os.path.dirname(mpl.matplotlib_fname())
                mpl_ttf_dir = os.path.join(mpl_data_dir, 'fonts', 'ttf')
    
                #Copy the font files to matplotlib's True Type font directory
                #(I originally tried to move the font files instead of copy them,
                #but it did not seem to work, so I gave up.)
                cp_ttf_dir = os.path.join(os.path.dirname(cp.__file__), 'true_type_fonts')
                for file_name in os.listdir(cp_ttf_dir):
                    if file_name[-4:] == '.ttf':
                        old_path = os.path.join(cp_ttf_dir, file_name)
                        new_path = os.path.join(mpl_ttf_dir, file_name)
                        shutil.copyfile(old_path, new_path)
                        print "Copying " + old_path + " -> " + new_path
    
                #Try to delete matplotlib's fontList cache
                mpl_cache_dir = mpl.get_cachedir()
                mpl_cache_dir_ls = os.listdir(mpl_cache_dir)
                if 'fontList.cache' in mpl_cache_dir_ls:
                    fontList_path = os.path.join(mpl_cache_dir, 'fontList.cache')
                    os.remove(fontList_path)
                    print "Deleted the matplotlib fontList.cache"
            except:
                warnings.warn("WARNING: An issue occured while installing the custom fonts for clearplot.")
    
    setup(...
        #Specify the dependencies and versions
        install_requires = ['matplotlib >= 1.4.0, !=1.4.3', 'numpy >= 1.6'],
        #Specify any non-python files to be distributed with the package
        package_data = {'' : ['color_maps/*.csv', 'true_type_fonts/*.ttf']},
        #Specify the custom install class
        cmdclass={'install' : move_ttf}
    )
    

    This solves both problem #1 (it installs matplotlib before it imports it) and problem #2 (it works with wheels).