You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何使用python -m build为Python包分发自定义matplotlib字体?

问题描述

我的Python包依赖matplotlib使用某一特定字体,该字体可能未安装在目标设备上。我希望在包构建完成后,将源码中的ttf字体文件安装到matplotlib的fonts/ttf目录中。

由于setuptools正逐步移除其部分CLI命令(如python setup.py sdist/bdist_wheel),我决定使用python -m build来构建我的包。

已尝试方案

我修改了一个过时的setup.py示例,希望适配python -m build工具:

setup.py代码:

import warnings
from functools import partial

from setuptools import setup
from setuptools.command.install import install

def _post_install():
    # 尝试安装自定义字体
    try:
        import os
        import shutil

        import matplotlib as mpl

        import mypkg

        # 找到matplotlib的True Type字体存储目录
        mpl_data_dir = os.path.dirname(mpl.matplotlib_fname())
        mpl_ttf_dir = os.path.join(mpl_data_dir, "fonts", "ttf")

        # 将字体文件复制到matplotlib的True Type字体目录
        # 原本尝试移动文件但未成功,改用复制
        cp_ttf_dir = os.path.join(os.path.dirname(mypkg.__file__), "ttf")
        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.move(old_path, new_path)
                print("moving" + old_path + " -> " + new_path)

        # 删除matplotlib的fontList缓存
        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(
            "An issue occured while installing the custom fonts for mypkg."
        )
        raise

# 子类化setuptools的install类,添加自定义安装后命令
class MoveTTF(install):
    def run(self):
        """
        执行常规安装流程后,将自定义字体复制到matplotlib的字体目录,
        并删除matplotlib的fontList缓存
        """
        # 执行常规安装
        install.run(self)
        self.execute(_post_install, (), "Moving Custom Font to matplotlib.")


setup_movettf = partial(
    setup,
    package_data={"": ["ttf/*.ttf"]},
    cmdclass={"install": MoveTTF},
)

setup_movettf()

我用partial包装setup,让build工具从pyproject.toml读取其他配置项:

pyproject.toml代码:

[build-system]
requires = ["setuptools>=61.0", "matplotlib"]
build-backend = "setuptools.build_meta"

[project]
name = "mypkg"
etc...

[tool.setuptools.packages.find]
where = ["src"]

[tool.setuptools.package-data]
mypkg = ["ttf/*.ttf"]
遇到的问题

使用python -m build时出现错误:

ModuleNotFoundError: No module named 'mypkg'

但用旧CLI命令python setup.py sdist和python setup.py bdist_wheel能成功构建wheel包,安装后(pip install dist/mypkg-[].whl)字体也能正确部署到matplotlib的ttf目录。

期望结果

希望python -m build能像旧setuptools CLI一样,在安装完成后成功导入mypkg并完成字体部署。


解决方案

问题根源

python -m build采用隔离构建环境,构建阶段就会执行setup.py代码,但此时你的包还未安装到构建环境中,因此无法导入mypkg;而旧CLI在当前环境执行,本地源码已在Python路径中,所以能正常导入。

方法一:修改安装逻辑,不依赖包导入

直接通过setuptools的安装上下文获取包的安装路径,无需导入mypkg:

  1. 修改_post_install函数,接收安装路径参数:
def _post_install(install_lib):
    # 尝试安装自定义字体
    try:
        import os
        import shutil
        import matplotlib as mpl

        # 直接用安装路径拼接字体目录
        cp_ttf_dir = os.path.join(install_lib, "mypkg", "ttf")
        
        # 后续字体复制、缓存清理逻辑保持不变
        mpl_data_dir = os.path.dirname(mpl.matplotlib_fname())
        mpl_ttf_dir = os.path.join(mpl_data_dir, "fonts", "ttf")
        
        for file_name in os.listdir(cp_ttf_dir):
            if file_name.endswith(".ttf"):
                old_path = os.path.join(cp_ttf_dir, file_name)
                new_path = os.path.join(mpl_ttf_dir, file_name)
                # 改用copy避免删除包内字体文件(后续更新包仍可复用)
                shutil.copy(old_path, new_path)
                print(f"copied {old_path} -> {new_path}")
        
        # 适配新版本matplotlib的缓存文件
        mpl_cache_dir = mpl.get_cachedir()
        for fname in os.listdir(mpl_cache_dir):
            if fname.startswith("fontlist") or fname == "fontList.cache":
                os.remove(os.path.join(mpl_cache_dir, fname))
                print(f"Deleted matplotlib cache file: {fname}")
    except Exception as e:
        warnings.warn(f"Failed to install custom fonts: {str(e)}")
        raise
  1. 在MoveTTF.run中传递安装路径:
class MoveTTF(install):
    def run(self):
        install.run(self)
        # 传递安装后的包目录给_post_install
        self.execute(_post_install, (self.install_lib,), "Moving Custom Font to matplotlib.")
  1. 删除_post_install中的import mypkg语句,避免构建阶段触发导入。

方法二:用entry_points实现规范的安装后钩子(推荐)

现代Python打包更推荐独立脚本+入口点的方式,避免构建阶段的依赖问题:

  1. 在包内创建mypkg/install_fonts.py,封装字体安装逻辑:
import os
import shutil
import warnings
import matplotlib as mpl

def install_fonts():
    try:
        # 获取包的安装目录
        pkg_dir = os.path.dirname(os.path.abspath(__file__))
        cp_ttf_dir = os.path.join(pkg_dir, "ttf")
        
        mpl_data_dir = os.path.dirname(mpl.matplotlib_fname())
        mpl_ttf_dir = os.path.join(mpl_data_dir, "fonts", "ttf")
        
        for file_name in os.listdir(cp_ttf_dir):
            if file_name.endswith(".ttf"):
                old_path = os.path.join(cp_ttf_dir, file_name)
                new_path = os.path.join(mpl_ttf_dir, file_name)
                shutil.copy(old_path, new_path)
                print(f"copied {old_path} -> {new_path}")
        
        # 清理matplotlib字体缓存
        mpl_cache_dir = mpl.get_cachedir()
        for fname in os.listdir(mpl_cache_dir):
            if fname.startswith("fontlist") or fname == "fontList.cache":
                os.remove(os.path.join(mpl_cache_dir, fname))
                print(f"Deleted matplotlib cache file: {fname}")
    except Exception as e:
        warnings.warn(f"Failed to install custom fonts: {str(e)}")
        raise
  1. 在pyproject.toml中添加控制台脚本入口:
[project.scripts]
mypkg-install-fonts = "mypkg.install_fonts:install_fonts"
  1. 删除setup.py中旧的MoveTTF类和_post_install函数,安装完成后用户可手动执行mypkg-install-fonts完成字体部署;若需自动执行,可在包的初始化逻辑中添加调用(注意权限问题,修改系统目录可能需要管理员权限)。

内容的提问来源于stack exchange,提问作者Siem

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.06 01:31:14