WIP: implement conditionSets in designspaceLib.RuleDescriptor (#1255)
* [designspaceLib] WIP: add conditionSets to RuleDescriptor... the BaseDocReader still needs to be updated to be able to read both legacy condition-without-conditionset and new-style conditionset elements. * test_rules: replace rule.conditions with conditionSets * Tests: update test.designspace to use conditionset; replace 'with' with 'byname' in sub element I don't like too much that now 'byname' precedes alphabetically 'name'. We need to see if ElementTree allows to sort attribute arbitrarily * Change versionnumber to 4.0 - would that be enough, or should there be seperate major / minor fields? If we bump the version up to numeral 4, I'd like the check for the with / byname attributes. We can read the old `byname` and only write the new `with`. Remove support for `initial` in axis attributes. * WIP - formatversion, with attribute, * WIP anything that is not version 4.0 is version 3.0 added some local paths to the test so I can see what it's generating. I can take this out later. * WIP read the conditionsets properly. This makes the tests fail no longer. * WIP disambiguate axis tags from axis names. Test axes are now called "axisName_x", tags are called "TAGX" * WIP forgot to push * WIP Raise `DesignSpaceDocumentError` when no maximum or minimum are given in a condition. Add test for such an incomplete rule. Remove path cruft. * WIP improved test of raised error. * WIP because no bot. * WIP more spaces around percents. * WIP - don't skip empty subs, just load whatever is there. * WIP do not permit empty axes in a location when evaluating a rule. Fix some tests. * WIP missed a str(tempdir), some rule tests. * WIP proposal for changes to the designspace spec to the `rule` and `sub` elements. * WIP edits. * WIP. Typo.
This commit is contained in:
parent
4da8f43eeb
commit
ceb41ec484
@ -84,17 +84,12 @@ Rules
|
||||
*****
|
||||
|
||||
Rules describe designspace areas in which one glyph should be replaced by another.
|
||||
A rule has a name and a number of conditionsets. The rule also contains a list of glyphname
|
||||
pairs: the glyphs that need to be substituted. For a rule to be triggered
|
||||
only one of the conditionsets needs to be true, ``OR``. Within a conditionset all
|
||||
conditions need to be true, ``AND``.
|
||||
A rule has a name and a number of conditionsets. The rule also contains a list of
|
||||
glyphname pairs: the glyphs that need to be substituted. For a rule to be triggered
|
||||
**only one** of the conditionsets needs to be true, ``OR``. Within a conditionset
|
||||
**all** conditions need to be true, ``AND``.
|
||||
|
||||
Variable fonts
|
||||
=======================
|
||||
|
||||
- In an variable font the substitution happens at run time: there are
|
||||
no changes in the font, only in the sequence of glyphnames that is
|
||||
rendered.
|
||||
The ``sub`` element contains a pair of glyphnames. The ``name`` attribute is the glyph that should be visible when the rule evaluates to **False**. The ``with`` attribute is the glyph that should be visible when the rule evaluates to **True**.
|
||||
|
||||
UFO instances
|
||||
=============
|
||||
@ -547,7 +542,7 @@ Attributes
|
||||
to the root path of this document. The path can be at the same level
|
||||
as the document or lower.
|
||||
- ``layer``: optional, string. The name of the layer in the source file.
|
||||
If no layer attribute is given assume it is the foreground layer.
|
||||
If no layer attribute is given assume the foreground layer should be used.
|
||||
|
||||
.. 31-lib-element:
|
||||
|
||||
@ -856,8 +851,11 @@ Example
|
||||
- Defines a named rule.
|
||||
- Each ``rule`` element contains one or more ``conditionset`` elements.
|
||||
- Only one ``conditionset`` needs to be true to trigger the rule.
|
||||
- All conditions must be true to make the ``conditionset`` true.
|
||||
- For backwards compatibility a ``rule`` can contain ``condition`` elements outside of a conditionset. These are then understood to be part of a single, implied, ``conditionset``.
|
||||
- All conditions in a ``conditionset`` must be true to make the ``conditionset`` true.
|
||||
- For backwards compatibility a ``rule`` can contain ``condition`` elements outside of a conditionset. These are then understood to be part of a single, implied, ``conditionset``. Note: these conditions should be written wrapped in a conditionset.
|
||||
- A rule element needs to contain one or more ``sub`` elements in order to be compiled to a variable font.
|
||||
- Rules without sub elements should be ignored when compiling a font.
|
||||
- For authoring tools it might be necessary to save designspace files without ``sub`` elements just because the work is incomplete.
|
||||
|
||||
.. attributes-11:
|
||||
|
||||
@ -865,7 +863,8 @@ Attributes
|
||||
----------
|
||||
|
||||
- ``name``: optional, string. A unique name that can be used to
|
||||
identify this rule if it needs to be referenced elsewhere.
|
||||
identify this rule if it needs to be referenced elsewhere. The name
|
||||
is not important for compiling variable fonts.
|
||||
|
||||
5.1.1 conditionset element
|
||||
=======================
|
||||
@ -879,7 +878,7 @@ Attributes
|
||||
=======================
|
||||
|
||||
- Child element of ``conditionset``
|
||||
- Between the ``minimum`` and ``maximum`` this rule is ``true``.
|
||||
- Between the ``minimum`` and ``maximum`` this rule is ``True``.
|
||||
- If ``minimum`` is not available, assume it is ``axis.minimum``.
|
||||
- If ``maximum`` is not available, assume it is ``axis.maximum``.
|
||||
- The condition must contain at least a minimum or maximum or both.
|
||||
@ -900,9 +899,7 @@ Attributes
|
||||
=================
|
||||
|
||||
- Child element of ``rule``.
|
||||
- Defines which glyphs to replace when the rule is true.
|
||||
- This element is optional. It may be useful for editors to know which
|
||||
glyphs can be used to preview the axis.
|
||||
- Defines which glyph to replace when the rule evaluates to **True**.
|
||||
|
||||
.. attributes-13:
|
||||
|
||||
@ -911,7 +908,7 @@ Attributes
|
||||
|
||||
- ``name``: string, required. The name of the glyph this rule looks
|
||||
for.
|
||||
- ``byname``: string, required. The name of the glyph it is replaced
|
||||
- ``with``: string, required. The name of the glyph it is replaced
|
||||
with.
|
||||
|
||||
.. example-7:
|
||||
@ -928,7 +925,7 @@ contained in a conditionset.
|
||||
<rule name="named.rule.1">
|
||||
<condition minimum="250" maximum="750" name="weight" />
|
||||
<condition minimum="50" maximum="100" name="width" />
|
||||
<sub name="dollar" byname="dollar.alt"/>
|
||||
<sub name="dollar" with="dollar.alt"/>
|
||||
</rule>
|
||||
</rules>
|
||||
|
||||
@ -946,7 +943,7 @@ Example with ``conditionsets``. All conditions in a conditionset must be true.
|
||||
<condition ... />
|
||||
<condition ... />
|
||||
</conditionset>
|
||||
<sub name="dollar" byname="dollar.alt"/>
|
||||
<sub name="dollar" with="dollar.alt"/>
|
||||
</rule>
|
||||
</rules>
|
||||
|
||||
|
@ -160,45 +160,49 @@ class SourceDescriptor(SimpleDescriptor):
|
||||
class RuleDescriptor(SimpleDescriptor):
|
||||
"""<!-- optional: list of substitution rules -->
|
||||
<rules>
|
||||
<rule name="vertical.bars" enabled="true">
|
||||
<sub name="cent" byname="cent.alt"/>
|
||||
<sub name="dollar" byname="dollar.alt"/>
|
||||
<condition tag="wght" minimum ="250.000000" maximum ="750.000000"/>
|
||||
<condition tag="wdth" minimum ="100"/>
|
||||
<condition tag="opsz" minimum="10" maximum="40"/>
|
||||
<rule name="vertical.bars">
|
||||
<conditionset>
|
||||
<condition minimum="250.000000" maximum="750.000000" name="weight"/>
|
||||
<condition minimum="100" name="width"/>
|
||||
<condition minimum="10" maximum="40" name="optical"/>
|
||||
</conditionset>
|
||||
<sub name="cent" with="cent.alt"/>
|
||||
<sub name="dollar" with="dollar.alt"/>
|
||||
</rule>
|
||||
</rules>
|
||||
|
||||
Discussion:
|
||||
use axis names rather than tags - then we can evaluate the rule without having to look up the axes.
|
||||
remove the subs from the rule.
|
||||
remove 'enabled' attr form rule
|
||||
"""
|
||||
_attrs = ['name', 'conditions', 'subs'] # what do we need here
|
||||
_attrs = ['name', 'conditionSets', 'subs'] # what do we need here
|
||||
def __init__(self):
|
||||
self.name = None
|
||||
self.conditions = [] # list of dict(tag='aaaa', minimum=0, maximum=1000)
|
||||
self.subs = [] # list of substitutions stored as tuples of glyphnames ("a", "a.alt")
|
||||
self.conditionSets = [] # list of list of dict(name='aaaa', minimum=0, maximum=1000)
|
||||
self.subs = [] # list of substitutions stored as tuples of glyphnames ("a", "a.alt")
|
||||
|
||||
|
||||
def evaluateRule(rule, location):
|
||||
""" Test if rule is True at location.maximum
|
||||
If a condition has no minimum, check for < maximum.
|
||||
If a condition has no maximum, check for > minimum.
|
||||
"""
|
||||
for cd in rule.conditions:
|
||||
if not cd['name'] in location:
|
||||
continue
|
||||
""" Return True if any of the rule's conditionsets matches the
|
||||
given location.
|
||||
"""
|
||||
return any(evaluateConditions(c, location) for c in rule.conditionSets)
|
||||
|
||||
|
||||
def evaluateConditions(conditions, location):
|
||||
""" Return True if all the conditions matches the given location.
|
||||
If a condition has no minimum, check for < maximum.
|
||||
If a condition has no maximum, check for > minimum.
|
||||
"""
|
||||
for cd in conditions:
|
||||
value = location[cd['name']]
|
||||
if cd.get('minimum') is None:
|
||||
if not location[cd['name']] <= cd['maximum']:
|
||||
if value > cd['maximum']:
|
||||
return False
|
||||
elif cd.get('maximum') is None:
|
||||
if not cd['minimum'] <= location[cd['name']]:
|
||||
return False
|
||||
else:
|
||||
if not cd['minimum'] <= location[cd['name']] <= cd['maximum']:
|
||||
if cd['minimum'] > value:
|
||||
return False
|
||||
elif not cd['minimum'] <= value <= cd['maximum']:
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def processRules(rules, location, glyphNames):
|
||||
""" Apply these rules at this location to these glyphnames.minimum
|
||||
- rule order matters
|
||||
@ -357,13 +361,9 @@ class BaseDocWriter(object):
|
||||
def __init__(self, documentPath, documentObject):
|
||||
self.path = documentPath
|
||||
self.documentObject = documentObject
|
||||
self.toolVersion = 3
|
||||
self.documentVersion = "4.0"
|
||||
self.root = ET.Element("designspace")
|
||||
self.root.attrib['format'] = "%d" % self.toolVersion
|
||||
#self.root.append(ET.Element("axes"))
|
||||
#self.root.append(ET.Element("rules"))
|
||||
#self.root.append(ET.Element("sources"))
|
||||
#self.root.append(ET.Element("instances"))
|
||||
self.root.attrib['format'] = self.documentVersion
|
||||
self.axes = []
|
||||
self.rules = []
|
||||
|
||||
@ -434,27 +434,33 @@ class BaseDocWriter(object):
|
||||
# if none of the conditions have minimum or maximum values, do not add the rule.
|
||||
self.rules.append(ruleObject)
|
||||
ruleElement = ET.Element('rule')
|
||||
ruleElement.attrib['name'] = ruleObject.name
|
||||
for cond in ruleObject.conditions:
|
||||
if cond.get('minimum') is None and cond.get('maximum') is None:
|
||||
# neither is defined, don't add this condition
|
||||
continue
|
||||
conditionElement = ET.Element('condition')
|
||||
conditionElement.attrib['name'] = cond.get('name')
|
||||
if cond.get('minimum') is not None:
|
||||
conditionElement.attrib['minimum'] = self.intOrFloat(cond.get('minimum'))
|
||||
if cond.get('maximum') is not None:
|
||||
conditionElement.attrib['maximum'] = self.intOrFloat(cond.get('maximum'))
|
||||
ruleElement.append(conditionElement)
|
||||
if ruleObject.name is not None:
|
||||
ruleElement.attrib['name'] = ruleObject.name
|
||||
for conditions in ruleObject.conditionSets:
|
||||
conditionsetElement = ET.Element('conditionset')
|
||||
for cond in conditions:
|
||||
if cond.get('minimum') is None and cond.get('maximum') is None:
|
||||
# neither is defined, don't add this condition
|
||||
continue
|
||||
conditionElement = ET.Element('condition')
|
||||
conditionElement.attrib['name'] = cond.get('name')
|
||||
if cond.get('minimum') is not None:
|
||||
conditionElement.attrib['minimum'] = self.intOrFloat(cond.get('minimum'))
|
||||
if cond.get('maximum') is not None:
|
||||
conditionElement.attrib['maximum'] = self.intOrFloat(cond.get('maximum'))
|
||||
conditionsetElement.append(conditionElement)
|
||||
if len(conditionsetElement):
|
||||
ruleElement.append(conditionsetElement)
|
||||
# XXX shouldn't we require at least one sub element?
|
||||
# if not ruleObject.subs:
|
||||
# raise DesignSpaceDocument('Invalid empty rule with no "sub" elements')
|
||||
for sub in ruleObject.subs:
|
||||
# skip empty subs
|
||||
if sub[0] == '' and sub[1] == '':
|
||||
continue
|
||||
subElement = ET.Element('sub')
|
||||
subElement.attrib['name'] = sub[0]
|
||||
subElement.attrib['with'] = sub[1]
|
||||
ruleElement.append(subElement)
|
||||
self.root.findall('.rules')[0].append(ruleElement)
|
||||
if len(ruleElement):
|
||||
self.root.findall('.rules')[0].append(ruleElement)
|
||||
|
||||
def _addAxis(self, axisObject):
|
||||
self.axes.append(axisObject)
|
||||
@ -648,10 +654,9 @@ class BaseDocReader(object):
|
||||
def __init__(self, documentPath, documentObject):
|
||||
self.path = documentPath
|
||||
self.documentObject = documentObject
|
||||
self.documentObject.formatVersion = 0
|
||||
tree = ET.parse(self.path)
|
||||
self.root = tree.getroot()
|
||||
self.documentObject.formatVersion = int(self.root.attrib.get("format", 0))
|
||||
self.documentObject.formatVersion = self.root.attrib.get("format", "3.0")
|
||||
self.axes = []
|
||||
self.rules = []
|
||||
self.sources = []
|
||||
@ -684,22 +689,32 @@ class BaseDocReader(object):
|
||||
for ruleElement in self.root.findall(".rules/rule"):
|
||||
ruleObject = self.ruleDescriptorClass()
|
||||
ruleObject.name = ruleElement.attrib.get("name")
|
||||
for conditionElement in ruleElement.findall('.condition'):
|
||||
cd = {}
|
||||
cdMin = conditionElement.attrib.get("minimum")
|
||||
if cdMin is not None:
|
||||
cd['minimum'] = float(cdMin)
|
||||
else:
|
||||
# will allow these to be None, assume axis.minimum
|
||||
cd['minimum'] = None
|
||||
cdMax = conditionElement.attrib.get("maximum")
|
||||
if cdMax is not None:
|
||||
cd['maximum'] = float(cdMax)
|
||||
else:
|
||||
# will allow these to be None, assume axis.maximum
|
||||
cd['maximum'] = None
|
||||
cd['name'] = conditionElement.attrib.get("name")
|
||||
ruleObject.conditions.append(cd)
|
||||
for conditionSetElement in ruleElement.findall('.conditionset'):
|
||||
cds = []
|
||||
for conditionElement in conditionSetElement.findall('.condition'):
|
||||
cd = {}
|
||||
cdMin = conditionElement.attrib.get("minimum")
|
||||
if cdMin is not None:
|
||||
cd['minimum'] = float(cdMin)
|
||||
else:
|
||||
# will allow these to be None, assume axis.minimum
|
||||
cd['minimum'] = None
|
||||
cdMax = conditionElement.attrib.get("maximum")
|
||||
if cdMax is not None:
|
||||
cd['maximum'] = float(cdMax)
|
||||
else:
|
||||
# will allow these to be None, assume axis.maximum
|
||||
cd['maximum'] = None
|
||||
cd['name'] = conditionElement.attrib.get("name")
|
||||
# test for things
|
||||
if cd.get('minimum') is None and cd.get('maximum') is None:
|
||||
if ruleObject.name is not None:
|
||||
n = ruleObject.name
|
||||
else:
|
||||
n = "%d" % len(rules)
|
||||
raise DesignSpaceDocumentError("No minimum or maximum defined in rule \"%s\"." % n)
|
||||
cds.append(cd)
|
||||
ruleObject.conditionSets.append(cds)
|
||||
for subElement in ruleElement.findall('.sub'):
|
||||
a = subElement.attrib['name']
|
||||
b = subElement.attrib['with']
|
||||
@ -721,15 +736,7 @@ class BaseDocReader(object):
|
||||
axisObject.maximum = float(axisElement.attrib.get("maximum"))
|
||||
if axisElement.attrib.get('hidden', False):
|
||||
axisObject.hidden = True
|
||||
# we need to check if there is an attribute named "initial"
|
||||
if axisElement.attrib.get("default") is None:
|
||||
if axisElement.attrib.get("initial") is not None:
|
||||
# stop doing this,
|
||||
axisObject.default = float(axisElement.attrib.get("initial"))
|
||||
else:
|
||||
axisObject.default = axisObject.minimum
|
||||
else:
|
||||
axisObject.default = float(axisElement.attrib.get("default"))
|
||||
axisObject.default = float(axisElement.attrib.get("default"))
|
||||
axisObject.tag = axisElement.attrib.get("tag")
|
||||
for mapElement in axisElement.findall('map'):
|
||||
a = float(mapElement.attrib['input'])
|
||||
@ -814,7 +821,7 @@ class BaseDocReader(object):
|
||||
sourceName = sourceElement.attrib.get('name')
|
||||
if sourceName is None:
|
||||
# add a temporary source name
|
||||
sourceName = "temp_master.%d"%(sourceCount)
|
||||
sourceName = "temp_master.%d" % (sourceCount)
|
||||
sourceObject = self.sourceDescriptorClass()
|
||||
sourceObject.path = sourcePath # absolute path to the ufo source
|
||||
sourceObject.filename = filename # path as it is stored in the document
|
||||
@ -1243,7 +1250,7 @@ class DesignSpaceDocument(object):
|
||||
if mutatorDefaultCandidate is not None:
|
||||
if mutatorDefaultCandidate.name != flaggedDefaultCandidate.name:
|
||||
# warn if we have a conflict
|
||||
self.logger.info("Note: conflicting default masters:\n\tUsing %s as default\n\tMutator found %s"%(flaggedDefaultCandidate.name, mutatorDefaultCandidate.name))
|
||||
self.logger.info("Note: conflicting default masters:\n\tUsing %s as default\n\tMutator found %s" % (flaggedDefaultCandidate.name, mutatorDefaultCandidate.name))
|
||||
self.default = flaggedDefaultCandidate
|
||||
self.defaultLoc = self.default.location
|
||||
else:
|
||||
@ -1268,7 +1275,7 @@ class DesignSpaceDocument(object):
|
||||
if axisObj.minimum <= neutralAxisValue <= axisObj.maximum:
|
||||
# yes we can fix this
|
||||
axisObj.default = neutralAxisValue
|
||||
self.logger.info("Note: updating the default value of axis %s to neutral master at %3.3f"%(axisObj.name, neutralAxisValue))
|
||||
self.logger.info("Note: updating the default value of axis %s to neutral master at %3.3f" % (axisObj.name, neutralAxisValue))
|
||||
# always fit the axis dimensions to the location of the designated neutral
|
||||
elif neutralAxisValue < axisObj.minimum:
|
||||
axisObj.default = neutralAxisValue
|
||||
@ -1278,7 +1285,7 @@ class DesignSpaceDocument(object):
|
||||
axisObj.default = neutralAxisValue
|
||||
else:
|
||||
# now we're in trouble, can't solve this, alert.
|
||||
self.logger.info("Warning: mismatched default value for axis %s and neutral master. Master value outside of axis bounds"%(axisObj.name))
|
||||
self.logger.info("Warning: mismatched default value for axis %s and neutral master. Master value outside of axis bounds" % (axisObj.name))
|
||||
|
||||
def getMutatorDefaultCandidate(self):
|
||||
# FIXME: original implementation using MutatorMath
|
||||
@ -1416,18 +1423,21 @@ class DesignSpaceDocument(object):
|
||||
axis.default = default
|
||||
# now the rules
|
||||
for rule in self.rules:
|
||||
newConditions = []
|
||||
for cond in rule.conditions:
|
||||
if cond.get('minimum') is not None:
|
||||
minimum = self.normalizeLocation({cond['name']:cond['minimum']}).get(cond['name'])
|
||||
else:
|
||||
minimum = None
|
||||
if cond.get('maximum') is not None:
|
||||
maximum = self.normalizeLocation({cond['name']:cond['maximum']}).get(cond['name'])
|
||||
else:
|
||||
maximum = None
|
||||
newConditions.append(dict(name=cond['name'], minimum=minimum, maximum=maximum))
|
||||
rule.conditions = newConditions
|
||||
newConditionSets = []
|
||||
for conditions in rule.conditionSets:
|
||||
newConditions = []
|
||||
for cond in conditions:
|
||||
if cond.get('minimum') is not None:
|
||||
minimum = self.normalizeLocation({cond['name']:cond['minimum']}).get(cond['name'])
|
||||
else:
|
||||
minimum = None
|
||||
if cond.get('maximum') is not None:
|
||||
maximum = self.normalizeLocation({cond['name']:cond['maximum']}).get(cond['name'])
|
||||
else:
|
||||
maximum = None
|
||||
newConditions.append(dict(name=cond['name'], minimum=minimum, maximum=maximum))
|
||||
newConditionSets.append(newConditions)
|
||||
rule.conditionSets = newConditionSets
|
||||
|
||||
|
||||
def rulesToFeature(doc, whiteSpace="\t", newLine="\n"):
|
||||
@ -1438,11 +1448,11 @@ def rulesToFeature(doc, whiteSpace="\t", newLine="\n"):
|
||||
axisDims = {axis.tag: (axis.minimum, axis.maximum) for axis in doc.axes}
|
||||
text = []
|
||||
for rule in doc.rules:
|
||||
text.append("rule %s{"%rule.name)
|
||||
text.append("rule %s{" % rule.name)
|
||||
for cd in rule.conditions:
|
||||
axisTag = axisNames.get(cd.get('name'), "****")
|
||||
axisMinimum = cd.get('minimum', axisDims.get(axisTag, [0,0])[0])
|
||||
axisMaximum = cd.get('maximum', axisDims.get(axisTag, [0,0])[1])
|
||||
text.append("%s%s %f %f;"%(whiteSpace, axisTag, axisMinimum, axisMaximum))
|
||||
text.append("} %s;"%rule.name)
|
||||
text.append("%s%s %f %f;" % (whiteSpace, axisTag, axisMinimum, axisMaximum))
|
||||
text.append("} %s;" % rule.name)
|
||||
return newLine.join(text)
|
||||
|
@ -1,5 +1,5 @@
|
||||
<?xml version='1.0' encoding='utf-8'?>
|
||||
<designspace format="3">
|
||||
<designspace format="4.0">
|
||||
<axes>
|
||||
<axis default="0" maximum="1000" minimum="0" name="weight" tag="wght">
|
||||
<labelname xml:lang="en">Wéíght</labelname>
|
||||
@ -14,8 +14,10 @@
|
||||
</axes>
|
||||
<rules>
|
||||
<rule name="named.rule.1">
|
||||
<condition maximum="1" minimum="0" name="aaaa" />
|
||||
<condition maximum="3" minimum="2" name="bbbb" />
|
||||
<conditionset>
|
||||
<condition maximum="1" minimum="0" name="axisName_a" />
|
||||
<condition maximum="3" minimum="2" name="axisName_b" />
|
||||
</conditionset>
|
||||
<sub name="a" with="a.alt" />
|
||||
</rule>
|
||||
</rules>
|
||||
|
@ -9,7 +9,7 @@ import pytest
|
||||
from fontTools.misc.py23 import open
|
||||
from fontTools.designspaceLib import (
|
||||
DesignSpaceDocument, SourceDescriptor, AxisDescriptor, RuleDescriptor,
|
||||
InstanceDescriptor, evaluateRule, processRules, posix)
|
||||
InstanceDescriptor, evaluateRule, processRules, posix, DesignSpaceDocumentError)
|
||||
|
||||
|
||||
def assert_equals_test_file(path, test_filename):
|
||||
@ -149,8 +149,10 @@ def test_fill_document(tmpdir):
|
||||
# write some rules
|
||||
r1 = RuleDescriptor()
|
||||
r1.name = "named.rule.1"
|
||||
r1.conditions.append(dict(name='aaaa', minimum=0, maximum=1))
|
||||
r1.conditions.append(dict(name='bbbb', minimum=2, maximum=3))
|
||||
r1.conditionSets.append([
|
||||
dict(name='axisName_a', minimum=0, maximum=1),
|
||||
dict(name='axisName_b', minimum=2, maximum=3)
|
||||
])
|
||||
r1.subs.append(("a", "a.alt"))
|
||||
doc.addRule(r1)
|
||||
# write the document
|
||||
@ -374,8 +376,10 @@ def test_localisedNames(tmpdir):
|
||||
# write some rules
|
||||
r1 = RuleDescriptor()
|
||||
r1.name = "named.rule.1"
|
||||
r1.conditions.append(dict(name='aaaa', minimum=0, maximum=1))
|
||||
r1.conditions.append(dict(name='bbbb', minimum=2, maximum=3))
|
||||
r1.conditionSets.append([
|
||||
dict(name='weight', minimum=200, maximum=500),
|
||||
dict(name='width', minimum=0, maximum=150)
|
||||
])
|
||||
r1.subs.append(("a", "a.alt"))
|
||||
doc.addRule(r1)
|
||||
# write the document
|
||||
@ -412,8 +416,8 @@ def test_handleNoAxes(tmpdir):
|
||||
a.minimum = 0
|
||||
a.maximum = 1000
|
||||
a.default = 0
|
||||
a.name = "axisName%s"%(name)
|
||||
a.tag = "ax_%d"%(value)
|
||||
a.name = "axisName%s" % (name)
|
||||
a.tag = "ax_%d" % (value)
|
||||
doc.addAxis(a)
|
||||
|
||||
# add master 1
|
||||
@ -458,7 +462,6 @@ def test_handleNoAxes(tmpdir):
|
||||
verify.read(testDocPath)
|
||||
verify.write(testDocPath2)
|
||||
|
||||
|
||||
def test_pathNameResolve(tmpdir):
|
||||
tmpdir = str(tmpdir)
|
||||
# test how descriptor.path and descriptor.filename are resolved
|
||||
@ -575,26 +578,26 @@ def test_normalise():
|
||||
a1.minimum = -1000
|
||||
a1.maximum = 1000
|
||||
a1.default = 0
|
||||
a1.name = "aaa"
|
||||
a1.tag = "aaaa"
|
||||
a1.name = "axisName_a"
|
||||
a1.tag = "TAGA"
|
||||
doc.addAxis(a1)
|
||||
|
||||
assert doc.normalizeLocation(dict(aaa=0)) == {'aaa': 0.0}
|
||||
assert doc.normalizeLocation(dict(aaa=1000)) == {'aaa': 1.0}
|
||||
assert doc.normalizeLocation(dict(axisName_a=0)) == {'axisName_a': 0.0}
|
||||
assert doc.normalizeLocation(dict(axisName_a=1000)) == {'axisName_a': 1.0}
|
||||
|
||||
# clipping beyond max values:
|
||||
assert doc.normalizeLocation(dict(aaa=1001)) == {'aaa': 1.0}
|
||||
assert doc.normalizeLocation(dict(aaa=500)) == {'aaa': 0.5}
|
||||
assert doc.normalizeLocation(dict(aaa=-1000)) == {'aaa': -1.0}
|
||||
assert doc.normalizeLocation(dict(aaa=-1001)) == {'aaa': -1.0}
|
||||
assert doc.normalizeLocation(dict(axisName_a=1001)) == {'axisName_a': 1.0}
|
||||
assert doc.normalizeLocation(dict(axisName_a=500)) == {'axisName_a': 0.5}
|
||||
assert doc.normalizeLocation(dict(axisName_a=-1000)) == {'axisName_a': -1.0}
|
||||
assert doc.normalizeLocation(dict(axisName_a=-1001)) == {'axisName_a': -1.0}
|
||||
# anisotropic coordinates normalise to isotropic
|
||||
assert doc.normalizeLocation(dict(aaa=(1000, -1000))) == {'aaa': 1.0}
|
||||
assert doc.normalizeLocation(dict(axisName_a=(1000, -1000))) == {'axisName_a': 1.0}
|
||||
doc.normalize()
|
||||
r = []
|
||||
for axis in doc.axes:
|
||||
r.append((axis.name, axis.minimum, axis.default, axis.maximum))
|
||||
r.sort()
|
||||
assert r == [('aaa', -1.0, 0.0, 1.0)]
|
||||
assert r == [('axisName_a', -1.0, 0.0, 1.0)]
|
||||
|
||||
doc = DesignSpaceDocument()
|
||||
# write some axes
|
||||
@ -602,24 +605,24 @@ def test_normalise():
|
||||
a2.minimum = 100
|
||||
a2.maximum = 1000
|
||||
a2.default = 100
|
||||
a2.name = "bbb"
|
||||
a2.name = "axisName_b"
|
||||
doc.addAxis(a2)
|
||||
assert doc.normalizeLocation(dict(bbb=0)) == {'bbb': 0.0}
|
||||
assert doc.normalizeLocation(dict(bbb=1000)) == {'bbb': 1.0}
|
||||
assert doc.normalizeLocation(dict(axisName_b=0)) == {'axisName_b': 0.0}
|
||||
assert doc.normalizeLocation(dict(axisName_b=1000)) == {'axisName_b': 1.0}
|
||||
# clipping beyond max values:
|
||||
assert doc.normalizeLocation(dict(bbb=1001)) == {'bbb': 1.0}
|
||||
assert doc.normalizeLocation(dict(bbb=500)) == {'bbb': 0.4444444444444444}
|
||||
assert doc.normalizeLocation(dict(bbb=-1000)) == {'bbb': 0.0}
|
||||
assert doc.normalizeLocation(dict(bbb=-1001)) == {'bbb': 0.0}
|
||||
assert doc.normalizeLocation(dict(axisName_b=1001)) == {'axisName_b': 1.0}
|
||||
assert doc.normalizeLocation(dict(axisName_b=500)) == {'axisName_b': 0.4444444444444444}
|
||||
assert doc.normalizeLocation(dict(axisName_b=-1000)) == {'axisName_b': 0.0}
|
||||
assert doc.normalizeLocation(dict(axisName_b=-1001)) == {'axisName_b': 0.0}
|
||||
# anisotropic coordinates normalise to isotropic
|
||||
assert doc.normalizeLocation(dict(bbb=(1000,-1000))) == {'bbb': 1.0}
|
||||
assert doc.normalizeLocation(dict(bbb=1001)) == {'bbb': 1.0}
|
||||
assert doc.normalizeLocation(dict(axisName_b=(1000,-1000))) == {'axisName_b': 1.0}
|
||||
assert doc.normalizeLocation(dict(axisName_b=1001)) == {'axisName_b': 1.0}
|
||||
doc.normalize()
|
||||
r = []
|
||||
for axis in doc.axes:
|
||||
r.append((axis.name, axis.minimum, axis.default, axis.maximum))
|
||||
r.sort()
|
||||
assert r == [('bbb', 0.0, 0.0, 1.0)]
|
||||
assert r == [('axisName_b', 0.0, 0.0, 1.0)]
|
||||
|
||||
doc = DesignSpaceDocument()
|
||||
# write some axes
|
||||
@ -687,15 +690,15 @@ def test_rules(tmpdir):
|
||||
doc = DesignSpaceDocument()
|
||||
# write some axes
|
||||
a1 = AxisDescriptor()
|
||||
a1.tag = "taga"
|
||||
a1.name = "aaaa"
|
||||
a1.tag = "TAGA"
|
||||
a1.name = "axisName_a"
|
||||
a1.minimum = 0
|
||||
a1.maximum = 1000
|
||||
a1.default = 0
|
||||
doc.addAxis(a1)
|
||||
a2 = AxisDescriptor()
|
||||
a2.tag = "tagb"
|
||||
a2.name = "bbbb"
|
||||
a2.tag = "TAGB"
|
||||
a2.name = "axisName_b"
|
||||
a2.minimum = 0
|
||||
a2.maximum = 3000
|
||||
a2.default = 0
|
||||
@ -703,80 +706,88 @@ def test_rules(tmpdir):
|
||||
|
||||
r1 = RuleDescriptor()
|
||||
r1.name = "named.rule.1"
|
||||
r1.conditions.append(dict(name='aaaa', minimum=0, maximum=1000))
|
||||
r1.conditions.append(dict(name='bbbb', minimum=0, maximum=3000))
|
||||
r1.conditionSets.append([
|
||||
dict(name='axisName_a', minimum=0, maximum=1000),
|
||||
dict(name='axisName_b', minimum=0, maximum=3000)
|
||||
])
|
||||
r1.subs.append(("a", "a.alt"))
|
||||
|
||||
# rule with minium and maximum
|
||||
doc.addRule(r1)
|
||||
assert len(doc.rules) == 1
|
||||
assert len(doc.rules[0].conditions) == 2
|
||||
assert evaluateRule(r1, dict(aaaa = 500, bbbb = 0)) == True
|
||||
assert evaluateRule(r1, dict(aaaa = 0, bbbb = 0)) == True
|
||||
assert evaluateRule(r1, dict(aaaa = 1000, bbbb = 0)) == True
|
||||
assert evaluateRule(r1, dict(aaaa = 1000, bbbb = -100)) == False
|
||||
assert evaluateRule(r1, dict(aaaa = 1000.0001, bbbb = 0)) == False
|
||||
assert evaluateRule(r1, dict(aaaa = -0.0001, bbbb = 0)) == False
|
||||
assert evaluateRule(r1, dict(aaaa = -100, bbbb = 0)) == False
|
||||
assert processRules([r1], dict(aaaa = 500), ["a", "b", "c"]) == ['a.alt', 'b', 'c']
|
||||
assert processRules([r1], dict(aaaa = 500), ["a.alt", "b", "c"]) == ['a.alt', 'b', 'c']
|
||||
assert processRules([r1], dict(aaaa = 2000), ["a", "b", "c"]) == ['a', 'b', 'c']
|
||||
assert len(doc.rules[0].conditionSets) == 1
|
||||
assert len(doc.rules[0].conditionSets[0]) == 2
|
||||
assert evaluateRule(r1, dict(axisName_a = 500, axisName_b = 0)) == True
|
||||
assert evaluateRule(r1, dict(axisName_a = 0, axisName_b = 0)) == True
|
||||
assert evaluateRule(r1, dict(axisName_a = 1000, axisName_b = 0)) == True
|
||||
assert evaluateRule(r1, dict(axisName_a = 1000, axisName_b = -100)) == False
|
||||
assert evaluateRule(r1, dict(axisName_a = 1000.0001, axisName_b = 0)) == False
|
||||
assert evaluateRule(r1, dict(axisName_a = -0.0001, axisName_b = 0)) == False
|
||||
assert evaluateRule(r1, dict(axisName_a = -100, axisName_b = 0)) == False
|
||||
assert processRules([r1], dict(axisName_a = 500, axisName_b = 0), ["a", "b", "c"]) == ['a.alt', 'b', 'c']
|
||||
assert processRules([r1], dict(axisName_a = 500, axisName_b = 0), ["a.alt", "b", "c"]) == ['a.alt', 'b', 'c']
|
||||
assert processRules([r1], dict(axisName_a = 2000, axisName_b = 0), ["a", "b", "c"]) == ['a', 'b', 'c']
|
||||
|
||||
# rule with only a maximum
|
||||
r2 = RuleDescriptor()
|
||||
r2.name = "named.rule.2"
|
||||
r2.conditions.append(dict(name='aaaa', maximum=500))
|
||||
r2.conditionSets.append([dict(name='axisName_a', maximum=500)])
|
||||
r2.subs.append(("b", "b.alt"))
|
||||
|
||||
assert evaluateRule(r2, dict(aaaa = 0)) == True
|
||||
assert evaluateRule(r2, dict(aaaa = -500)) == True
|
||||
assert evaluateRule(r2, dict(aaaa = 1000)) == False
|
||||
assert evaluateRule(r2, dict(axisName_a = 0)) == True
|
||||
assert evaluateRule(r2, dict(axisName_a = -500)) == True
|
||||
assert evaluateRule(r2, dict(axisName_a = 1000)) == False
|
||||
|
||||
# rule with only a minimum
|
||||
r3 = RuleDescriptor()
|
||||
r3.name = "named.rule.3"
|
||||
r3.conditions.append(dict(name='aaaa', minimum=500))
|
||||
r3.conditionSets.append([dict(name='axisName_a', minimum=500)])
|
||||
r3.subs.append(("c", "c.alt"))
|
||||
|
||||
assert evaluateRule(r3, dict(aaaa = 0)) == False
|
||||
assert evaluateRule(r3, dict(aaaa = 1000)) == True
|
||||
assert evaluateRule(r3, dict(bbbb = 1000)) == True
|
||||
assert evaluateRule(r3, dict(axisName_a = 0)) == False
|
||||
assert evaluateRule(r3, dict(axisName_a = 1000)) == True
|
||||
assert evaluateRule(r3, dict(axisName_a = 1000)) == True
|
||||
|
||||
# rule with only a minimum, maximum in separate conditions
|
||||
r4 = RuleDescriptor()
|
||||
r4.name = "named.rule.4"
|
||||
r4.conditions.append(dict(name='aaaa', minimum=500))
|
||||
r4.conditions.append(dict(name='bbbb', maximum=500))
|
||||
r4.conditionSets.append([
|
||||
dict(name='axisName_a', minimum=500),
|
||||
dict(name='axisName_b', maximum=500)
|
||||
])
|
||||
r4.subs.append(("c", "c.alt"))
|
||||
|
||||
assert evaluateRule(r4, dict()) == True # is this what we expect though?
|
||||
assert evaluateRule(r4, dict(aaaa = 1000, bbbb = 0)) == True
|
||||
assert evaluateRule(r4, dict(aaaa = 0, bbbb = 0)) == False
|
||||
assert evaluateRule(r4, dict(aaaa = 1000, bbbb = 1000)) == False
|
||||
assert evaluateRule(r4, dict(axisName_a = 1000, axisName_b = 0)) == True
|
||||
assert evaluateRule(r4, dict(axisName_a = 0, axisName_b = 0)) == False
|
||||
assert evaluateRule(r4, dict(axisName_a = 1000, axisName_b = 1000)) == False
|
||||
|
||||
a1 = AxisDescriptor()
|
||||
a1.minimum = 0
|
||||
a1.maximum = 1000
|
||||
a1.default = 0
|
||||
a1.name = "aaaa"
|
||||
a1.tag = "aaaa"
|
||||
a1.name = "axisName_a"
|
||||
a1.tag = "TAGA"
|
||||
b1 = AxisDescriptor()
|
||||
b1.minimum = 2000
|
||||
b1.maximum = 3000
|
||||
b1.default = 2000
|
||||
b1.name = "bbbb"
|
||||
b1.tag = "bbbb"
|
||||
b1.name = "axisName_b"
|
||||
b1.tag = "TAGB"
|
||||
doc.addAxis(a1)
|
||||
doc.addAxis(b1)
|
||||
assert doc._prepAxesForBender() == {'aaaa': {'map': [], 'name': 'aaaa', 'default': 0, 'minimum': 0, 'maximum': 1000, 'tag': 'aaaa'}, 'bbbb': {'map': [], 'name': 'bbbb', 'default': 2000, 'minimum': 2000, 'maximum': 3000, 'tag': 'bbbb'}}
|
||||
assert doc._prepAxesForBender() == {'axisName_a': {'map': [], 'name': 'axisName_a', 'default': 0, 'minimum': 0, 'maximum': 1000, 'tag': 'TAGA'}, 'axisName_b': {'map': [], 'name': 'axisName_b', 'default': 2000, 'minimum': 2000, 'maximum': 3000, 'tag': 'TAGB'}}
|
||||
|
||||
assert doc.rules[0].conditions == [{'minimum': 0, 'maximum': 1000, 'name': 'aaaa'}, {'minimum': 0, 'maximum': 3000, 'name': 'bbbb'}]
|
||||
assert doc.rules[0].conditionSets == [[
|
||||
{'minimum': 0, 'maximum': 1000, 'name': 'axisName_a'},
|
||||
{'minimum': 0, 'maximum': 3000, 'name': 'axisName_b'}]]
|
||||
|
||||
assert doc.rules[0].subs == [('a', 'a.alt')]
|
||||
|
||||
doc.normalize()
|
||||
assert doc.rules[0].name == 'named.rule.1'
|
||||
assert doc.rules[0].conditions == [{'minimum': 0.0, 'maximum': 1.0, 'name': 'aaaa'}, {'minimum': 0.0, 'maximum': 1.0, 'name': 'bbbb'}]
|
||||
assert doc.rules[0].conditionSets == [[
|
||||
{'minimum': 0.0, 'maximum': 1.0, 'name': 'axisName_a'},
|
||||
{'minimum': 0.0, 'maximum': 1.0, 'name': 'axisName_b'}]]
|
||||
|
||||
doc.write(testDocPath)
|
||||
new = DesignSpaceDocument()
|
||||
@ -787,6 +798,36 @@ def test_rules(tmpdir):
|
||||
new.write(testDocPath2)
|
||||
|
||||
|
||||
def test_incompleteRule(tmpdir):
|
||||
tmpdir = str(tmpdir)
|
||||
testDocPath1 = os.path.join(tmpdir, "testIncompleteRule.designspace")
|
||||
doc = DesignSpaceDocument()
|
||||
r1 = RuleDescriptor()
|
||||
r1.name = "incomplete.rule.1"
|
||||
r1.conditionSets.append([
|
||||
dict(name='axisName_a', minimum=100),
|
||||
dict(name='axisName_b', maximum=200)
|
||||
])
|
||||
r1.subs.append(("c", "c.alt"))
|
||||
doc.addRule(r1)
|
||||
doc.write(testDocPath1)
|
||||
__removeConditionMinimumMaximumDesignSpace(testDocPath1)
|
||||
new = DesignSpaceDocument()
|
||||
with pytest.raises(DesignSpaceDocumentError) as excinfo:
|
||||
new.read(testDocPath1)
|
||||
assert "No minimum or maximum defined in rule" in str(excinfo.value)
|
||||
|
||||
def __removeConditionMinimumMaximumDesignSpace(path):
|
||||
# only for testing, so we can make an invalid designspace file
|
||||
# without making the designSpaceDocument also support it.
|
||||
f = open(path, 'r', encoding='utf-8')
|
||||
d = f.read()
|
||||
f.close()
|
||||
d = d.replace(' minimum="100"', '')
|
||||
f = open(path, 'w', encoding='utf-8')
|
||||
f.write(d)
|
||||
f.close()
|
||||
|
||||
def __removeAxesFromDesignSpace(path):
|
||||
# only for testing, so we can make an invalid designspace file
|
||||
# without making the designSpaceDocument also support it.
|
||||
|
Loading…
x
Reference in New Issue
Block a user