Merge pull request #1821 from fonttools/varLib-wrap-exceptions

[varLib] Use proper exceptions for signalling input source errors
This commit is contained in:
Nikolaus Waxweiler 2020-02-13 15:24:22 +00:00 committed by GitHub
commit 4be6aaf71c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 211 additions and 93 deletions

View File

@ -39,13 +39,11 @@ import os.path
import logging
from copy import deepcopy
from pprint import pformat
from .errors import VarLibError, VarLibValidationError
log = logging.getLogger("fontTools.varLib")
class VarLibError(Exception):
pass
#
# Creation routines
#
@ -81,7 +79,12 @@ def _add_fvar(font, axes, instances):
coordinates = instance.location
if "en" not in instance.localisedStyleName:
assert instance.styleName
if not instance.styleName:
raise VarLibValidationError(
f"Instance at location '{coordinates}' must have a default English "
"style name ('stylename' attribute on the instance element or a "
"stylename element with an 'xml:lang=\"en\"' attribute)."
)
localisedStyleName = dict(instance.localisedStyleName)
localisedStyleName["en"] = tounicode(instance.styleName)
else:
@ -137,20 +140,32 @@ def _add_avar(font, axes):
# Current avar requirements. We don't have to enforce
# these on the designer and can deduce some ourselves,
# but for now just enforce them.
assert axis.minimum == min(keys)
assert axis.maximum == max(keys)
assert axis.default in keys
# No duplicates
assert len(set(keys)) == len(keys), (
f"{axis.tag} axis: All axis mapping input='...' "
"values must be unique, but we found duplicates."
)
assert len(set(vals)) == len(vals), (
f"{axis.tag} axis: All axis mapping output='...' "
"values must be unique, but we found duplicates."
)
if axis.minimum != min(keys):
raise VarLibValidationError(
f"Axis '{axis.name}': there must be a mapping for the axis minimum "
f"value {axis.minimum} and it must be the lowest input mapping value."
)
if axis.maximum != max(keys):
raise VarLibValidationError(
f"Axis '{axis.name}': there must be a mapping for the axis maximum "
f"value {axis.maximum} and it must be the highest input mapping value."
)
if axis.default not in keys:
raise VarLibValidationError(
f"Axis '{axis.name}': there must be a mapping for the axis default "
f"value {axis.default}."
)
# No duplicate input values (output values can be >= their preceeding value).
if len(set(keys)) != len(keys):
raise VarLibValidationError(
f"Axis '{axis.name}': All axis mapping input='...' values must be "
"unique, but we found duplicates."
)
# Ascending values
assert sorted(vals) == vals
if sorted(vals) != vals:
raise VarLibValidationError(
f"Axis '{axis.name}': mapping output values must be in ascending order."
)
keys_triple = (axis.minimum, axis.default, axis.maximum)
vals_triple = tuple(axis.map_forward(v) for v in keys_triple)
@ -214,8 +229,8 @@ def _add_stat(font, axes):
def _add_gvar(font, masterModel, master_ttfs, tolerance=0.5, optimize=True):
assert tolerance >= 0
if tolerance < 0:
raise ValueError("`tolerance` must be a positive number.")
log.info("Generating gvar")
assert "gvar" not in font
@ -703,9 +718,10 @@ def load_designspace(designspace):
masters = ds.sources
if not masters:
raise VarLibError("no sources found in .designspace")
raise VarLibValidationError("Designspace must have at least one source.")
instances = ds.instances
# TODO: Use fontTools.designspaceLib.tagForAxisName instead.
standard_axis_map = OrderedDict([
('weight', ('wght', {'en': u'Weight'})),
('width', ('wdth', {'en': u'Width'})),
@ -715,11 +731,15 @@ def load_designspace(designspace):
])
# Setup axes
if not ds.axes:
raise VarLibValidationError(f"Designspace must have at least one axis.")
axes = OrderedDict()
for axis in ds.axes:
for axis_index, axis in enumerate(ds.axes):
axis_name = axis.name
if not axis_name:
assert axis.tag is not None
if not axis.tag:
raise VarLibValidationError(f"Axis at index {axis_index} needs a tag.")
axis_name = axis.name = axis.tag
if axis_name in standard_axis_map:
@ -728,7 +748,8 @@ def load_designspace(designspace):
if not axis.labelNames:
axis.labelNames.update(standard_axis_map[axis_name][1])
else:
assert axis.tag is not None
if not axis.tag:
raise VarLibValidationError(f"Axis at index {axis_index} needs a tag.")
if not axis.labelNames:
axis.labelNames["en"] = tounicode(axis_name)
@ -739,15 +760,28 @@ def load_designspace(designspace):
for obj in masters+instances:
obj_name = obj.name or obj.styleName or ''
loc = obj.location
if loc is None:
raise VarLibValidationError(
f"Source or instance '{obj_name}' has no location."
)
for axis_name in loc.keys():
assert axis_name in axes, "Location axis '%s' unknown for '%s'." % (axis_name, obj_name)
if axis_name not in axes:
raise VarLibValidationError(
f"Location axis '{axis_name}' unknown for '{obj_name}'."
)
for axis_name,axis in axes.items():
if axis_name not in loc:
# NOTE: `axis.default` is always user-space, but `obj.location` always design-space.
loc[axis_name] = axis.map_forward(axis.default)
else:
v = axis.map_backward(loc[axis_name])
assert axis.minimum <= v <= axis.maximum, "Location for axis '%s' (mapped to %s) out of range for '%s' [%s..%s]" % (axis_name, v, obj_name, axis.minimum, axis.maximum)
if not (axis.minimum <= v <= axis.maximum):
raise VarLibValidationError(
f"Source or instance '{obj_name}' has out-of-range location "
f"for axis '{axis_name}': is mapped to {v} but must be in "
f"mapped range [{axis.minimum}..{axis.maximum}] (NOTE: all "
"values are in user-space)."
)
# Normalize master locations
@ -768,9 +802,15 @@ def load_designspace(designspace):
base_idx = None
for i,m in enumerate(normalized_master_locs):
if all(v == 0 for v in m.values()):
assert base_idx is None
if base_idx is not None:
raise VarLibValidationError(
"More than one base master found in Designspace."
)
base_idx = i
assert base_idx is not None, "Base master not found; no master at default location?"
if base_idx is None:
raise VarLibValidationError(
"Base master not found; no master at default location?"
)
log.info("Index of base master: %s", base_idx)
return _DesignSpaceData(
@ -927,7 +967,7 @@ def _open_font(path, master_finder=lambda s: s):
elif tp in ("TTF", "OTF", "WOFF", "WOFF2"):
font = TTFont(master_path)
else:
raise VarLibError("Invalid master path: %r" % master_path)
raise VarLibValidationError("Invalid master path: %r" % master_path)
return font
@ -947,10 +987,10 @@ def load_masters(designspace, master_finder=lambda s: s):
# If a SourceDescriptor has a layer name, demand that the compiled TTFont
# be supplied by the caller. This spares us from modifying MasterFinder.
if master.layerName and master.font is None:
raise AttributeError(
"Designspace source '%s' specified a layer name but lacks the "
"required TTFont object in the 'font' attribute."
% (master.name or "<Unknown>")
raise VarLibValidationError(
f"Designspace source '{master.name or '<Unknown>'}' specified a "
"layer name but lacks the required TTFont object in the 'font' "
"attribute."
)
return designspace.loadSourceFonts(_open_font, master_finder=master_finder)

View File

@ -1,5 +1,4 @@
from collections import namedtuple
import os
from fontTools.cffLib import (
maxStackLimit,
TopDictIndex,
@ -21,6 +20,13 @@ from fontTools.varLib.models import allEqual
from fontTools.misc.psCharStrings import T2CharString, T2OutlineExtractor
from fontTools.pens.t2CharStringPen import T2CharStringPen, t2c_round
from .errors import VarLibCFFDictMergeError, VarLibCFFPointTypeMergeError, VarLibMergeError
# Backwards compatibility
MergeDictError = VarLibCFFDictMergeError
MergeTypeError = VarLibCFFPointTypeMergeError
def addCFFVarStore(varFont, varModel, varDataList, masterSupports):
fvarTable = varFont['fvar']
@ -126,16 +132,6 @@ def convertCFFtoCFF2(varFont):
del varFont['CFF ']
class MergeDictError(TypeError):
def __init__(self, key, value, values):
error_msg = ["For the Private Dict key '{}', ".format(key),
"the default font value list:",
"\t{}".format(value),
"had a different number of values than a region font:"]
error_msg += ["\t{}".format(region_value) for region_value in values]
error_msg = os.linesep.join(error_msg)
def conv_to_int(num):
if isinstance(num, float) and num.is_integer():
return int(num)
@ -219,7 +215,7 @@ def merge_PrivateDicts(top_dicts, vsindex_dict, var_model, fd_map):
try:
values = zip(*values)
except IndexError:
raise MergeDictError(key, value, values)
raise VarLibCFFDictMergeError(key, value, values)
"""
Row 0 contains the first value from each master.
Convert each row from absolute values to relative
@ -426,21 +422,6 @@ def merge_charstrings(glyphOrder, num_masters, top_dicts, masterModel):
return cvData
class MergeTypeError(TypeError):
def __init__(self, point_type, pt_index, m_index, default_type, glyphName):
self.error_msg = [
"In glyph '{gname}' "
"'{point_type}' at point index {pt_index} in master "
"index {m_index} differs from the default font point "
"type '{default_type}'"
"".format(
gname=glyphName,
point_type=point_type, pt_index=pt_index,
m_index=m_index, default_type=default_type)
][0]
super(MergeTypeError, self).__init__(self.error_msg)
def makeRoundNumberFunc(tolerance):
if tolerance < 0:
raise ValueError("Rounding tolerance must be positive")
@ -547,7 +528,7 @@ class CFF2CharStringMergePen(T2CharStringPen):
else:
cmd = self._commands[self.pt_index]
if cmd[0] != point_type:
raise MergeTypeError(
raise VarLibCFFPointTypeMergeError(
point_type,
self.pt_index, len(cmd[1]),
cmd[0], self.glyphName)
@ -560,7 +541,7 @@ class CFF2CharStringMergePen(T2CharStringPen):
else:
cmd = self._commands[self.pt_index]
if cmd[0] != hint_type:
raise MergeTypeError(hint_type, self.pt_index, len(cmd[1]),
raise VarLibCFFPointTypeMergeError(hint_type, self.pt_index, len(cmd[1]),
cmd[0], self.glyphName)
cmd[1].append(args)
self.pt_index += 1
@ -576,7 +557,7 @@ class CFF2CharStringMergePen(T2CharStringPen):
else:
cmd = self._commands[self.pt_index]
if cmd[0] != hint_type:
raise MergeTypeError(hint_type, self.pt_index, len(cmd[1]),
raise VarLibCFFPointTypeMergeError(hint_type, self.pt_index, len(cmd[1]),
cmd[0], self.glyphName)
self.pt_index += 1
cmd = self._commands[self.pt_index]
@ -646,8 +627,8 @@ class CFF2CharStringMergePen(T2CharStringPen):
# second has only args.
if lastOp in ['hintmask', 'cntrmask']:
coord = list(cmd[1])
assert allEqual(coord), (
"hintmask values cannot differ between source fonts.")
if not allEqual(coord):
raise VarLibMergeError("Hintmask values cannot differ between source fonts.")
cmd[1] = [coord[0][0]]
else:
coords = cmd[1]

View File

@ -0,0 +1,39 @@
class VarLibError(Exception):
"""Base exception for the varLib module."""
class VarLibValidationError(VarLibError):
"""Raised when input data is invalid from varLib's point of view."""
class VarLibMergeError(VarLibError):
"""Raised when input data cannot be merged into a variable font."""
class VarLibCFFDictMergeError(VarLibMergeError):
"""Raised when a CFF PrivateDict cannot be merged."""
def __init__(self, key, value, values):
error_msg = (
f"For the Private Dict key '{key}', the default font value list:"
f"\n\t{value}\nhad a different number of values than a region font:"
)
for region_value in values:
error_msg += f"\n\t{region_value}"
self.args = (error_msg,)
class VarLibCFFPointTypeMergeError(VarLibMergeError):
"""Raised when a CFF glyph cannot be merged."""
def __init__(self, point_type, pt_index, m_index, default_type, glyph_name):
error_msg = (
f"Glyph '{glyph_name}': '{point_type}' at point index {pt_index} in "
f"master index {m_index} differs from the default font point type "
f"'{default_type}'"
)
self.args = (error_msg,)
class VariationModelError(VarLibError):
"""Raised when a variation model is faulty."""

View File

@ -10,6 +10,8 @@ from fontTools.ttLib.tables import otTables as ot
from fontTools.otlLib.builder import buildLookup, buildSingleSubstSubtable
from collections import OrderedDict
from .errors import VarLibValidationError
def addFeatureVariations(font, conditionalSubstitutions, featureTag='rvrn'):
"""Add conditional substitutions to a Variable Font.
@ -312,7 +314,10 @@ def addFeatureVariationsRaw(font, conditionalSubstitutions, featureTag='rvrn'):
for conditionSet, substitutions in conditionalSubstitutions:
conditionTable = []
for axisTag, (minValue, maxValue) in sorted(conditionSet.items()):
assert minValue < maxValue
if minValue > maxValue:
raise VarLibValidationError(
"A condition set has a minimum value above the maximum value."
)
ct = buildConditionTable(axisIndices[axisTag], minValue, maxValue)
conditionTable.append(ct)

View File

@ -14,6 +14,8 @@ from fontTools.varLib.varStore import VarStoreInstancer
from functools import reduce
from fontTools.otlLib.builder import buildSinglePos
from .errors import VarLibMergeError
class Merger(object):
@ -66,8 +68,8 @@ class Merger(object):
if hasattr(item, "ensureDecompiled"):
item.ensureDecompiled()
keys = sorted(vars(out).keys())
assert all(keys == sorted(vars(v).keys()) for v in lst), \
(keys, [sorted(vars(v).keys()) for v in lst])
if not all(keys == sorted(vars(v).keys()) for v in lst):
raise VarLibMergeError((keys, [sorted(vars(v).keys()) for v in lst]))
mergers = self.mergersFor(out)
defaultMerger = mergers.get('*', self.__class__.mergeThings)
try:
@ -82,7 +84,8 @@ class Merger(object):
raise
def mergeLists(self, out, lst):
assert allEqualTo(out, lst, len), (len(out), [len(v) for v in lst])
if not allEqualTo(out, lst, len):
raise VarLibMergeError((len(out), [len(v) for v in lst]))
for i,(value,values) in enumerate(zip(out, zip(*lst))):
try:
self.mergeThings(value, values)
@ -92,7 +95,8 @@ class Merger(object):
def mergeThings(self, out, lst):
try:
assert allEqualTo(out, lst, type), (out, lst)
if not allEqualTo(out, lst, type):
raise VarLibMergeError((out, lst))
mergerFunc = self.mergersFor(out).get(None, None)
if mergerFunc is not None:
mergerFunc(self, out, lst)
@ -101,7 +105,8 @@ class Merger(object):
elif isinstance(out, list):
self.mergeLists(out, lst)
else:
assert allEqualTo(out, lst), (out, lst)
if not allEqualTo(out, lst):
raise VarLibMergeError((out, lst))
except Exception as e:
e.args = e.args + (type(out).__name__,)
raise
@ -122,7 +127,8 @@ class AligningMerger(Merger):
@AligningMerger.merger(ot.GDEF, "GlyphClassDef")
def merge(merger, self, lst):
if self is None:
assert allNone(lst), (lst)
if not allNone(lst):
raise VarLibMergeError(lst)
return
lst = [l.classDefs for l in lst]
@ -134,7 +140,8 @@ def merge(merger, self, lst):
allKeys.update(*[l.keys() for l in lst])
for k in allKeys:
allValues = nonNone(l.get(k) for l in lst)
assert allEqual(allValues), allValues
if not allEqual(allValues):
raise VarLibMergeError(allValues)
if not allValues:
self[k] = None
else:
@ -170,7 +177,8 @@ def _merge_GlyphOrders(font, lst, values_lst=None, default=None):
sortKey = font.getReverseGlyphMap().__getitem__
order = sorted(combined, key=sortKey)
# Make sure all input glyphsets were in proper order
assert all(sorted(vs, key=sortKey) == vs for vs in lst), "glyph orders are not consistent across masters"
if not all(sorted(vs, key=sortKey) == vs for vs in lst):
raise VarLibMergeError("Glyph order inconsistent across masters.")
del combined
paddedValues = None
@ -197,7 +205,10 @@ def _Lookup_SinglePos_get_effective_value(subtables, glyph):
elif self.Format == 2:
return self.Value[self.Coverage.glyphs.index(glyph)]
else:
assert 0
raise VarLibMergeError(
"Cannot retrieve effective value for SinglePos lookup, unsupported "
f"format {self.Format}."
)
return None
def _Lookup_PairPos_get_effective_value_pair(subtables, firstGlyph, secondGlyph):
@ -219,13 +230,17 @@ def _Lookup_PairPos_get_effective_value_pair(subtables, firstGlyph, secondGlyph)
klass2 = self.ClassDef2.classDefs.get(secondGlyph, 0)
return self.Class1Record[klass1].Class2Record[klass2]
else:
assert 0
raise VarLibMergeError(
"Cannot retrieve effective value pair for PairPos lookup, unsupported "
f"format {self.Format}."
)
return None
@AligningMerger.merger(ot.SinglePos)
def merge(merger, self, lst):
self.ValueFormat = valueFormat = reduce(int.__or__, [l.ValueFormat for l in lst], 0)
assert len(lst) == 1 or (valueFormat & ~0xF == 0), valueFormat
if not (len(lst) == 1 or (valueFormat & ~0xF == 0)):
raise VarLibMergeError(f"SinglePos format {valueFormat} is unsupported.")
# If all have same coverage table and all are format 1,
coverageGlyphs = self.Coverage.glyphs
@ -511,7 +526,9 @@ def merge(merger, self, lst):
elif self.Format == 2:
_PairPosFormat2_merge(self, lst, merger)
else:
assert False
raise VarLibMergeError(
f"Cannot merge PairPos lookup, unsupported format {self.Format}."
)
del merger.valueFormat1, merger.valueFormat2
@ -576,7 +593,8 @@ def _MarkBasePosFormat1_merge(self, lst, merger, Mark='Mark', Base='Base'):
# failures in that case will probably signify mistakes in the
# input masters.
assert allEqual(allClasses), allClasses
if not allEqual(allClasses):
raise VarLibMergeError(allClasses)
if not allClasses:
rec = None
else:
@ -625,19 +643,31 @@ def _MarkBasePosFormat1_merge(self, lst, merger, Mark='Mark', Base='Base'):
@AligningMerger.merger(ot.MarkBasePos)
def merge(merger, self, lst):
assert allEqualTo(self.Format, (l.Format for l in lst))
if not allEqualTo(self.Format, (l.Format for l in lst)):
raise VarLibMergeError(
f"MarkBasePos formats inconsistent across masters, "
f"expected {self.Format} but got {[l.Format for l in lst]}."
)
if self.Format == 1:
_MarkBasePosFormat1_merge(self, lst, merger)
else:
assert False
raise VarLibMergeError(
f"Cannot merge MarkBasePos lookup, unsupported format {self.Format}."
)
@AligningMerger.merger(ot.MarkMarkPos)
def merge(merger, self, lst):
assert allEqualTo(self.Format, (l.Format for l in lst))
if not allEqualTo(self.Format, (l.Format for l in lst)):
raise VarLibMergeError(
f"MarkMarkPos formats inconsistent across masters, "
f"expected {self.Format} but got {[l.Format for l in lst]}."
)
if self.Format == 1:
_MarkBasePosFormat1_merge(self, lst, merger, 'Mark1', 'Mark2')
else:
assert False
raise VarLibMergeError(
f"Cannot merge MarkMarkPos lookup, unsupported format {self.Format}."
)
def _PairSet_flatten(lst, font):
@ -766,8 +796,16 @@ def merge(merger, self, lst):
if not sts:
continue
if sts[0].__class__.__name__.startswith('Extension'):
assert allEqual([st.__class__ for st in sts])
assert allEqual([st.ExtensionLookupType for st in sts])
if not allEqual([st.__class__ for st in sts]):
raise VarLibMergeError(
"Use of extensions inconsistent between masters: "
f"{[st.__class__.__name__ for st in sts]}."
)
if not allEqual([st.ExtensionLookupType for st in sts]):
raise VarLibMergeError(
"Extension lookup type differs between masters: "
f"{[st.ExtensionLookupType for st in sts]}."
)
l.LookupType = sts[0].ExtensionLookupType
new_sts = [st.ExtSubTable for st in sts]
del sts[:]
@ -995,7 +1033,8 @@ class VariationMerger(AligningMerger):
masterModel = None
if None in lst:
if allNone(lst):
assert out is None, (out, lst)
if out is not None:
raise VarLibMergeError((out, lst))
return
masterModel = self.model
model, lst = masterModel.getSubModel(lst)
@ -1015,7 +1054,8 @@ def buildVarDevTable(store_builder, master_values):
@VariationMerger.merger(ot.CaretValue)
def merge(merger, self, lst):
assert self.Format == 1
if self.Format != 1:
raise VarLibMergeError(f"CaretValue format {self.Format} unsupported.")
self.Coordinate, DeviceTable = buildVarDevTable(merger.store_builder, [a.Coordinate for a in lst])
if DeviceTable:
self.Format = 3
@ -1023,7 +1063,8 @@ def merge(merger, self, lst):
@VariationMerger.merger(ot.Anchor)
def merge(merger, self, lst):
assert self.Format == 1
if self.Format != 1:
raise VarLibMergeError(f"Anchor format {self.Format} unsupported.")
self.XCoordinate, XDeviceTable = buildVarDevTable(merger.store_builder, [a.XCoordinate for a in lst])
self.YCoordinate, YDeviceTable = buildVarDevTable(merger.store_builder, [a.YCoordinate for a in lst])
if XDeviceTable or YDeviceTable:

View File

@ -5,6 +5,8 @@ __all__ = ['nonNone', 'allNone', 'allEqual', 'allEqualTo', 'subList',
'supportScalar',
'VariationModel']
from .errors import VariationModelError
def nonNone(lst):
return [l for l in lst if l is not None]
@ -43,7 +45,11 @@ def normalizeValue(v, triple):
0.5
"""
lower, default, upper = triple
assert lower <= default <= upper, "invalid axis values: %3.3f, %3.3f %3.3f"%(lower, default, upper)
if not (lower <= default <= upper):
raise ValueError(
f"Invalid axis values, must be minimum, default, maximum: "
f"{lower:3.3f}, {default:3.3f}, {upper:3.3f}"
)
v = max(min(v, upper), lower)
if v == default:
v = 0.
@ -192,7 +198,7 @@ class VariationModel(object):
def __init__(self, locations, axisOrder=None):
if len(set(tuple(sorted(l.items())) for l in locations)) != len(locations):
raise ValueError("locations must be unique")
raise VariationModelError("Locations must be unique.")
self.origLocations = locations
self.axisOrder = axisOrder if axisOrder is not None else []
@ -220,7 +226,8 @@ class VariationModel(object):
@staticmethod
def getMasterLocationsSortKeyFunc(locations, axisOrder=[]):
assert {} in locations, "Base master not found."
if {} not in locations:
raise VariationModelError("Base master not found.")
axisPoints = {}
for loc in locations:
if len(loc) != 1:

View File

@ -2,6 +2,10 @@
the known glyph set, unless a glyph set was not provided.
- [varLib] When filling in the default axis value for a missing location of a source or
instance, correctly map the value forward.
- [varLib] The avar table can now contain mapping output values that are greater than
OR EQUAL to the preceeding value, as the avar specification allows this.
- [varLib] The errors of the module are now ordered hierarchically below VarLibError.
See #1821.
4.3.0 (released 2020-02-03)
---------------------------

View File

@ -1,6 +1,6 @@
from fontTools.misc.py23 import *
from fontTools.varLib.models import (
normalizeLocation, supportScalar, VariationModel)
normalizeLocation, supportScalar, VariationModel, VariationModelError)
import pytest
@ -145,7 +145,7 @@ class VariationModelTest(object):
assert model.deltaWeights == deltaWeights
def test_init_duplicate_locations(self):
with pytest.raises(ValueError, match="locations must be unique"):
with pytest.raises(VariationModelError, match="Locations must be unique."):
VariationModel(
[
{"foo": 0.0, "bar": 0.0},

View File

@ -1,6 +1,7 @@
from fontTools.misc.py23 import *
from fontTools.ttLib import TTFont, newTable
from fontTools.varLib import build, load_designspace
from fontTools.varLib.errors import VarLibValidationError
from fontTools.varLib.mutator import instantiateVariableFont
from fontTools.varLib import main as varLib_main, load_masters
from fontTools.varLib import set_default_weight_width_slant
@ -744,7 +745,7 @@ def test_load_masters_layerName_without_required_font():
ds.addSource(s)
with pytest.raises(
AttributeError,
VarLibValidationError,
match="specified a layer name but lacks the required TTFont object",
):
load_masters(ds)