diff --git a/Doc/source/designspaceLib/readme.rst b/Doc/source/designspaceLib/readme.rst
index 3aad060be..9eaf67772 100644
--- a/Doc/source/designspaceLib/readme.rst
+++ b/Doc/source/designspaceLib/readme.rst
@@ -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.
-
+
@@ -946,7 +943,7 @@ Example with ``conditionsets``. All conditions in a conditionset must be true.
-
+
diff --git a/Lib/fontTools/designspaceLib/__init__.py b/Lib/fontTools/designspaceLib/__init__.py
index 3be15e33b..6d2d84d62 100644
--- a/Lib/fontTools/designspaceLib/__init__.py
+++ b/Lib/fontTools/designspaceLib/__init__.py
@@ -160,45 +160,49 @@ class SourceDescriptor(SimpleDescriptor):
class RuleDescriptor(SimpleDescriptor):
"""
-
-
-
-
-
-
+
+
+
+
+
+
+
+
-
- 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)
diff --git a/Tests/designspaceLib/data/test.designspace b/Tests/designspaceLib/data/test.designspace
index 43116deb2..cf7056b33 100644
--- a/Tests/designspaceLib/data/test.designspace
+++ b/Tests/designspaceLib/data/test.designspace
@@ -1,5 +1,5 @@
-
+
Wéíght
@@ -14,8 +14,10 @@
-
-
+
+
+
+
diff --git a/Tests/designspaceLib/designspace_test.py b/Tests/designspaceLib/designspace_test.py
index 5ee09b27d..256e45742 100644
--- a/Tests/designspaceLib/designspace_test.py
+++ b/Tests/designspaceLib/designspace_test.py
@@ -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.