From e732310caa659b20d8b160f294196aee341cf851 Mon Sep 17 00:00:00 2001 From: Antony Lee Date: Sat, 13 Jan 2018 16:07:16 -0800 Subject: [PATCH 1/2] Cache paths of fonts shipped with mpl relative to the mpl data path. This lets the font cache stay valid across multiple virtualenvs (as long as the list of fonts shipped by mpl does not change). --- lib/matplotlib/font_manager.py | 75 ++++++++++++++++++++++------------ 1 file changed, 48 insertions(+), 27 deletions(-) diff --git a/lib/matplotlib/font_manager.py b/lib/matplotlib/font_manager.py index b8688860c9d4..ce1d921dced0 100644 --- a/lib/matplotlib/font_manager.py +++ b/lib/matplotlib/font_manager.py @@ -44,6 +44,7 @@ from dummy_threading import Timer import warnings +import matplotlib as mpl from matplotlib import afm, cbook, ft2font, rcParams, get_cachedir from matplotlib.fontconfig_pattern import ( parse_fontconfig_pattern, generate_fontconfig_pattern) @@ -828,15 +829,23 @@ def copy(self): class JSONEncoder(json.JSONEncoder): def default(self, o): if isinstance(o, FontManager): - return dict(o.__dict__, _class='FontManager') + return dict(o.__dict__, __class__='FontManager') elif isinstance(o, FontEntry): - return dict(o.__dict__, _class='FontEntry') + d = dict(o.__dict__, __class__='FontEntry') + try: + # Cache paths of fonts shipped with mpl relative to the mpl + # data path, which helps in the presence of venvs. + d["fname"] = str( + Path(d["fname"]).relative_to(mpl.get_data_path())) + except ValueError: + pass + return d else: return super().default(o) def _json_decode(o): - cls = o.pop('_class', None) + cls = o.pop('__class__', None) if cls is None: return o elif cls == 'FontManager': @@ -846,15 +855,21 @@ def _json_decode(o): elif cls == 'FontEntry': r = FontEntry.__new__(FontEntry) r.__dict__.update(o) + if not os.path.isabs(r.fname): + r.fname = os.path.join(mpl.get_data_path(), r.fname) return r else: - raise ValueError("don't know how to deserialize _class=%s" % cls) + raise ValueError("don't know how to deserialize __class__=%s" % cls) def json_dump(data, filename): - """Dumps a data structure as JSON in the named file. - Handles FontManager and its fields.""" + """ + Dumps a data structure as JSON in the named file. + Handles FontManager and its fields. File paths that are children of the + Matplotlib data path (typically, fonts shipped with Matplotlib) are stored + relative to that data path (to remain valid across virtualenvs). + """ with open(filename, 'w') as fh: try: json.dump(data, fh, cls=JSONEncoder, indent=2) @@ -863,9 +878,13 @@ def json_dump(data, filename): def json_load(filename): - """Loads a data structure as JSON from the named file. - Handles FontManager and its fields.""" + """ + Loads a data structure as JSON from the named file. + Handles FontManager and its fields. Relative file paths are interpreted + as being relative to the Matplotlib data path, and transformed into + absolute paths. + """ with open(filename, 'r') as fh: return json.load(fh, object_hook=_json_decode) @@ -951,30 +970,32 @@ def __init__(self, size=None, weight='normal'): _log.debug('font search path %s', str(paths)) # Load TrueType fonts and create font dictionary. - self.ttffiles = findSystemFonts(paths) + findSystemFonts() self.defaultFamily = { 'ttf': 'DejaVu Sans', 'afm': 'Helvetica'} self.defaultFont = {} - for fname in self.ttffiles: - _log.debug('trying fontname %s', fname) - if fname.lower().find('DejaVuSans.ttf')>=0: - self.defaultFont['ttf'] = fname - break - else: - # use anything - self.defaultFont['ttf'] = self.ttffiles[0] - - self.ttflist = createFontList(self.ttffiles) - - self.afmfiles = (findSystemFonts(paths, fontext='afm') - + findSystemFonts(fontext='afm')) - self.afmlist = createFontList(self.afmfiles, fontext='afm') - if len(self.afmfiles): - self.defaultFont['afm'] = self.afmfiles[0] - else: - self.defaultFont['afm'] = None + ttffiles = findSystemFonts(paths) + findSystemFonts() + self.defaultFont['ttf'] = next( + (fname for fname in ttffiles + if fname.lower().endswith("dejavusans.ttf")), + ttffiles[0]) + self.ttflist = createFontList(ttffiles) + + afmfiles = (findSystemFonts(paths, fontext='afm') + + findSystemFonts(fontext='afm')) + self.afmlist = createFontList(afmfiles, fontext='afm') + self.defaultFont['afm'] = afmfiles[0] if afmfiles else None + + @property + @cbook.deprecated("3.0") + def ttffiles(self): + return [font.fname for font in self.ttflist] + + @property + @cbook.deprecated("3.0") + def afmfiles(self): + return [font.fname for font in self.afmlist] def get_default_weight(self): """ From 9f821d1d37e7a35038fd7b6a564c63d095fc9399 Mon Sep 17 00:00:00 2001 From: Antony Lee Date: Mon, 9 Jul 2018 00:16:10 +0200 Subject: [PATCH 2/2] Include cache version in fontlist cache filename. --- .flake8 | 2 +- lib/matplotlib/font_manager.py | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/.flake8 b/.flake8 index 4c64cd4eada0..0103d1ff3001 100644 --- a/.flake8 +++ b/.flake8 @@ -42,7 +42,7 @@ per-file-ignores = matplotlib/backends/qt_editor/formlayout.py: E301, E501 matplotlib/backends/tkagg.py: E231, E302, E701 matplotlib/backends/windowing.py: E301, E302 - matplotlib/font_manager.py: E203, E221, E225, E251, E261, E262, E302, E501 + matplotlib/font_manager.py: E203, E221, E251, E261, E262, E302, E501 matplotlib/fontconfig_pattern.py: E201, E203, E221, E222, E225, E302 matplotlib/legend_handler.py: E201, E501 matplotlib/mathtext.py: E201, E202, E203, E211, E221, E222, E225, E231, E251, E261, E301, E302, E303, E402, E501 diff --git a/lib/matplotlib/font_manager.py b/lib/matplotlib/font_manager.py index ce1d921dced0..e451dfdb281f 100644 --- a/lib/matplotlib/font_manager.py +++ b/lib/matplotlib/font_manager.py @@ -945,7 +945,7 @@ class FontManager(object): # Increment this version number whenever the font cache data # format or behavior has changed and requires a existing font # cache files to be rebuilt. - __version__ = 201 + __version__ = 300 def __init__(self, size=None, weight='normal'): self._version = self.__version__ @@ -1333,7 +1333,8 @@ def findfont(prop, fontext='ttf'): cachedir = get_cachedir() if cachedir is not None: - _fmcache = os.path.join(cachedir, 'fontList.json') + _fmcache = os.path.join( + cachedir, 'fontlist-v{}.json'.format(FontManager.__version__)) fontManager = None