如何使用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:
- 修改
_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
- 在
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.")
- 删除
_post_install中的import mypkg语句,避免构建阶段触发导入。
方法二:用entry_points实现规范的安装后钩子(推荐)
现代Python打包更推荐独立脚本+入口点的方式,避免构建阶段的依赖问题:
- 在包内创建
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
- 在
pyproject.toml中添加控制台脚本入口:
[project.scripts] mypkg-install-fonts = "mypkg.install_fonts:install_fonts"
- 删除
setup.py中旧的MoveTTF类和_post_install函数,安装完成后用户可手动执行mypkg-install-fonts完成字体部署;若需自动执行,可在包的初始化逻辑中添加调用(注意权限问题,修改系统目录可能需要管理员权限)。
内容的提问来源于stack exchange,提问作者Siem

