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 b8688860c9d4..e451dfdb281f 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) @@ -926,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__ @@ -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] + 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) - self.ttflist = createFontList(self.ttffiles) + afmfiles = (findSystemFonts(paths, fontext='afm') + + findSystemFonts(fontext='afm')) + self.afmlist = createFontList(afmfiles, fontext='afm') + self.defaultFont['afm'] = afmfiles[0] if afmfiles else None - 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 + @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): """ @@ -1312,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