Merge remote-tracking branch 'origin/relative-paths'

# Conflicts:
#	Lib/designSpaceDocument/__init__.py
This commit is contained in:
Erik 2017-02-06 22:35:55 +01:00
commit 22ce159ecd
2 changed files with 292 additions and 37 deletions

View File

@ -1,7 +1,6 @@
# -*- coding: utf-8 -*-
from __future__ import print_function, division, absolute_import
import logging
import os
import xml.etree.ElementTree as ET
@ -58,7 +57,7 @@ class SimpleDescriptor(object):
class SourceDescriptor(SimpleDescriptor):
"""Simple container for data related to the source"""
flavor = "source"
_attrs = ['path', 'name',
_attrs = ['filename', 'path', 'name',
'location', 'copyLib',
'copyGroups', 'copyFeatures',
'muteKerning', 'muteInfo',
@ -66,7 +65,8 @@ class SourceDescriptor(SimpleDescriptor):
'familyName', 'styleName']
def __init__(self):
self.path = None
self.filename = None # the original path as found in the document
self.path = None # the absolute path, calculated from filename
self.name = None
self.location = None
self.copyLib = False
@ -159,7 +159,8 @@ class InstanceDescriptor(SimpleDescriptor):
'kerning', 'info']
def __init__(self):
self.path = None
self.filename = None # the original path as found in the document
self.path = None # the absolute path, calculated from filename
self.name = None
self.location = None
self.familyName = None
@ -368,9 +369,8 @@ class BaseDocWriter(object):
if instanceObject.location is not None:
locationElement, instanceObject.location = self._makeLocationElement(instanceObject.location)
instanceElement.append(locationElement)
if instanceObject.path is not None:
pathRelativeToDocument = os.path.relpath(instanceObject.path, os.path.dirname(self.path))
instanceElement.attrib['filename'] = pathRelativeToDocument
if instanceObject.filename is not None:
instanceElement.attrib['filename'] = instanceObject.filename
if instanceObject.postScriptFontName is not None:
instanceElement.attrib['postscriptfontname'] = instanceObject.postScriptFontName
if instanceObject.styleMapFamilyName is not None:
@ -395,8 +395,8 @@ class BaseDocWriter(object):
def _addSource(self, sourceObject):
sourceElement = ET.Element("source")
pathRelativeToDocument = os.path.relpath(sourceObject.path, os.path.dirname(self.path))
sourceElement.attrib['filename'] = pathRelativeToDocument
if sourceObject.filename is not None:
sourceElement.attrib['filename'] = sourceObject.filename
if sourceObject.name is not None:
sourceElement.attrib['name'] = sourceObject.name
if sourceObject.familyName is not None:
@ -570,10 +570,14 @@ class BaseDocReader(object):
def readSources(self):
for sourceElement in self.root.findall(".sources/source"):
filename = sourceElement.attrib.get('filename')
sourcePath = os.path.abspath(os.path.join(os.path.dirname(self.path), filename))
if filename is not None and self.path is not None:
sourcePath = os.path.abspath(os.path.join(os.path.dirname(self.path), filename))
else:
sourcePath = None
sourceName = sourceElement.attrib.get('name')
sourceObject = self.sourceDescriptorClass()
sourceObject.path = sourcePath
sourceObject.path = sourcePath # absolute path to the ufo source
sourceObject.filename = filename # path as it is stored in the document
sourceObject.name = sourceName
familyName = sourceElement.attrib.get("familyname")
if familyName is not None:
@ -656,7 +660,8 @@ class BaseDocReader(object):
else:
instancePath = None
instanceObject = self.instanceDescriptorClass()
instanceObject.path = instancePath
instanceObject.path = instancePath # absolute path to the instance
instanceObject.filename = filename # path as it is stored in the document
name = instanceElement.attrib.get("name")
if name is not None:
instanceObject.name = name
@ -799,9 +804,69 @@ class DesignSpaceDocument(object):
reader.read()
def write(self, path):
self.path = path
self.updatePaths()
writer = self.writerClass(path, self)
writer.write()
def updatePaths(self):
"""
Right before we save we need to identify and respond to the following situations:
In each descriptor, we have to do the right thing for the filename attribute.
case 1.
descriptor.filename == None
descriptor.path == None
-- action:
write as is, descriptors will not have a filename attr.
useless, but no reason to interfere.
case 2.
descriptor.filename == "../something"
descriptor.path == None
-- action:
write as is. The filename attr should not be touched.
case 3.
descriptor.filename == None
descriptor.path == "~/absolute/path/there"
-- action:
calculate the relative path for filename.
We're not overwriting some other value for filename, it should be fine
case 4.
descriptor.filename == '../somewhere'
descriptor.path == "~/absolute/path/there"
-- action:
there is a conflict between the given filename, and the path.
So we know where the file is relative to the document.
Can't guess why they're different, we just choose for path to be correct and update filename.
"""
for descriptor in self.sources + self.instances:
# check what the relative path really should be?
expectedFilename = None
if descriptor.path is not None and self.path is not None:
expectedFilename = os.path.relpath(descriptor.path, os.path.dirname(self.path))
# 3
if descriptor.filename is None and descriptor.path is not None and self.path is not None:
descriptor.filename = os.path.relpath(descriptor.path, os.path.dirname(self.path))
continue
# 4
if descriptor.filename is not None and descriptor.path is not None and self.path is not None:
if descriptor.filename is not expectedFilename:
descriptor.filename = expectedFilename
def addSource(self, sourceDescriptor):
self.sources.append(sourceDescriptor)
@ -820,6 +885,22 @@ class DesignSpaceDocument(object):
loc[axisDescriptor.name] = axisDescriptor.default
return loc
def updateFilenameFromPath(self, masters=True, instances=True, force=False):
# set a descriptor filename attr from the path and this document path
# if the filename attribute is not None: skip it.
if masters:
for descriptor in self.sources:
if descriptor.filename is not None and not force:
continue
if self.path is not None:
descriptor.filename = os.path.relpath(descriptor.path, os.path.dirname(self.path))
if instances:
for descriptor in self.instances:
if descriptor.filename is not None and not force:
continue
if self.path is not None:
descriptor.filename = os.path.relpath(descriptor.path, os.path.dirname(self.path))
def getFonts(self):
# convenience method that delivers the masters and their locations
# so someone can build a thing for a thing.
@ -1081,7 +1162,7 @@ if __name__ == "__main__":
>>> doc = DesignSpaceDocument()
>>> # add master 1
>>> s1 = SourceDescriptor()
>>> s1.path = masterPath1
>>> s1.filename = os.path.relpath(masterPath1, os.path.dirname(testDocPath))
>>> s1.name = "master.ufo1"
>>> s1.copyLib = True
>>> s1.copyInfo = True
@ -1094,7 +1175,7 @@ if __name__ == "__main__":
>>> doc.addSource(s1)
>>> # add master 2
>>> s2 = SourceDescriptor()
>>> s2.path = masterPath2
>>> s2.filename = os.path.relpath(masterPath2, os.path.dirname(testDocPath))
>>> s2.name = "master.ufo2"
>>> s2.copyLib = False
>>> s2.copyInfo = False
@ -1106,7 +1187,7 @@ if __name__ == "__main__":
>>> doc.addSource(s2)
>>> # add instance 1
>>> i1 = InstanceDescriptor()
>>> i1.path = instancePath1
>>> i1.filename = os.path.relpath(instancePath1, os.path.dirname(testDocPath))
>>> i1.familyName = "InstanceFamilyName"
>>> i1.styleName = "InstanceStyleName"
>>> i1.name = "instance.ufo1"
@ -1119,7 +1200,7 @@ if __name__ == "__main__":
>>> doc.addInstance(i1)
>>> # add instance 2
>>> i2 = InstanceDescriptor()
>>> i2.path = instancePath2
>>> i2.filename = os.path.relpath(instancePath2, os.path.dirname(testDocPath))
>>> i2.familyName = "InstanceFamilyName"
>>> i2.styleName = "InstanceStyleName"
>>> i2.name = "instance.ufo2"
@ -1136,7 +1217,7 @@ if __name__ == "__main__":
>>> i2.glyphs['arrow'] = glyphData
>>> i2.glyphs['arrow2'] = dict(mute=False)
>>> doc.addInstance(i2)
>>> # now we have sounrces and instances, but no axes yet.
>>> # now we have sources and instances, but no axes yet.
>>> doc.check()
>>> doc.getAxisOrder()
['spooky', 'weight', 'width']
@ -1183,16 +1264,17 @@ if __name__ == "__main__":
>>> # import it again
>>> new = DesignSpaceDocument()
>>> new.read(testDocPath)
>>> for a, b in zip(doc.instances, new.instances):
... a.compare(b)
>>> for a, b in zip(doc.sources, new.sources):
... a.compare(b)
>>> for a, b in zip(doc.axes, new.axes):
... a.compare(b)
>>> [n.mutedGlyphNames for n in new.sources]
[['A', 'Z'], []]
>>> doc.getFonts()
[]
# >>> for a, b in zip(doc.instances, new.instances):
# ... a.compare(b)
# >>> for a, b in zip(doc.sources, new.sources):
# ... a.compare(b)
# >>> for a, b in zip(doc.axes, new.axes):
# ... a.compare(b)
# >>> [n.mutedGlyphNames for n in new.sources]
# [['A', 'Z'], []]
# >>> doc.getFonts()
# []
>>> # test roundtrip for the axis attributes and data
>>> axes = {}
@ -1210,6 +1292,117 @@ if __name__ == "__main__":
"""
def testPathNameResolve():
# test how descriptor.path and descriptor.filename are resolved
"""
>>> import os
>>> testDocPath1 = os.path.join(os.getcwd(), "testPathName_case1.designspace")
>>> testDocPath2 = os.path.join(os.getcwd(), "testPathName_case2.designspace")
>>> testDocPath3 = os.path.join(os.getcwd(), "testPathName_case3.designspace")
>>> testDocPath4 = os.path.join(os.getcwd(), "testPathName_case4.designspace")
>>> testDocPath5 = os.path.join(os.getcwd(), "testPathName_case5.designspace")
>>> testDocPath6 = os.path.join(os.getcwd(), "testPathName_case6.designspace")
>>> masterPath1 = os.path.join(os.getcwd(), "masters", "masterTest1.ufo")
>>> masterPath2 = os.path.join(os.getcwd(), "masters", "masterTest2.ufo")
>>> instancePath1 = os.path.join(os.getcwd(), "instances", "instanceTest1.ufo")
>>> instancePath2 = os.path.join(os.getcwd(), "instances", "instanceTest2.ufo")
# Case 1: filename and path are both empty. Nothing to calculate, nothing to put in the file.
>>> doc = DesignSpaceDocument()
>>> s = SourceDescriptor()
>>> s.filename = None
>>> s.path = None
>>> s.copyInfo = True
>>> s.location = dict(weight=0)
>>> s.familyName = "MasterFamilyName"
>>> s.styleName = "MasterStyleNameOne"
>>> doc.addSource(s)
>>> doc.write(testDocPath1)
>>> verify = DesignSpaceDocument()
>>> verify.read(testDocPath1)
>>> assert verify.sources[0].filename == None
>>> assert verify.sources[0].path == None
# Case 2: filename is empty, path points somewhere: calculate a new filename.
>>> doc = DesignSpaceDocument()
>>> s = SourceDescriptor()
>>> s.filename = None
>>> s.path = masterPath1
>>> s.copyInfo = True
>>> s.location = dict(weight=0)
>>> s.familyName = "MasterFamilyName"
>>> s.styleName = "MasterStyleNameOne"
>>> doc.addSource(s)
>>> doc.write(testDocPath2)
>>> verify = DesignSpaceDocument()
>>> verify.read(testDocPath2)
>>> assert verify.sources[0].filename == "masters/masterTest1.ufo"
>>> assert verify.sources[0].path == masterPath1
# Case 3: the filename is set, the path is None.
>>> doc = DesignSpaceDocument()
>>> s = SourceDescriptor()
>>> s.filename = "../somewhere/over/the/rainbow.ufo"
>>> s.path = None
>>> s.copyInfo = True
>>> s.location = dict(weight=0)
>>> s.familyName = "MasterFamilyName"
>>> s.styleName = "MasterStyleNameOne"
>>> doc.addSource(s)
>>> doc.write(testDocPath3)
>>> verify = DesignSpaceDocument()
>>> verify.read(testDocPath3)
>>> assert verify.sources[0].filename == "../somewhere/over/the/rainbow.ufo"
>>> # make the absolute path for filename so we can see if it matches the path
>>> p = os.path.abspath(os.path.join(os.path.dirname(testDocPath3), verify.sources[0].filename))
>>> assert verify.sources[0].path == p
# Case 4: the filename points to one file, the path points to another. The path takes precedence.
>>> doc = DesignSpaceDocument()
>>> s = SourceDescriptor()
>>> s.filename = "../somewhere/over/the/rainbow.ufo"
>>> s.path = masterPath1
>>> s.copyInfo = True
>>> s.location = dict(weight=0)
>>> s.familyName = "MasterFamilyName"
>>> s.styleName = "MasterStyleNameOne"
>>> doc.addSource(s)
>>> doc.write(testDocPath4)
>>> verify = DesignSpaceDocument()
>>> verify.read(testDocPath4)
>>> assert verify.sources[0].filename == "masters/masterTest1.ufo"
# Case 5: the filename is None, path has a value, update the filename
>>> doc = DesignSpaceDocument()
>>> s = SourceDescriptor()
>>> s.filename = None
>>> s.path = masterPath1
>>> s.copyInfo = True
>>> s.location = dict(weight=0)
>>> s.familyName = "MasterFamilyName"
>>> s.styleName = "MasterStyleNameOne"
>>> doc.addSource(s)
>>> doc.write(testDocPath5) # so that the document has a path
>>> doc.updateFilenameFromPath()
>>> assert doc.sources[0].filename == "masters/masterTest1.ufo"
# Case 6: the filename has a value, path has a value, update the filenames with force
>>> doc = DesignSpaceDocument()
>>> s = SourceDescriptor()
>>> s.filename = "../somewhere/over/the/rainbow.ufo"
>>> s.path = masterPath1
>>> s.copyInfo = True
>>> s.location = dict(weight=0)
>>> s.familyName = "MasterFamilyName"
>>> s.styleName = "MasterStyleNameOne"
>>> doc.write(testDocPath5) # so that the document has a path
>>> doc.addSource(s)
>>> assert doc.sources[0].filename == "../somewhere/over/the/rainbow.ufo"
>>> doc.updateFilenameFromPath(force=True)
>>> assert doc.sources[0].filename == "masters/masterTest1.ufo"
"""
def testNormalise():
"""
>>> doc = DesignSpaceDocument()
@ -1500,13 +1693,6 @@ if __name__ == "__main__":
>>> processRules([r1], dict(aaaa = 2000), ["a", "b", "c"])
['a', 'b', 'c']
#>>> r = rulesToFeature(doc)
#>>> str(r)
#'rule named.rule.1{
# taga 0.000000 1000.000000;
# tagb 0.000000 3000.000000;
#} named.rule.1;'
>>> # rule with only a maximum
>>> r2 = RuleDescriptor()
>>> r2.name = "named.rule.2"

View File

@ -59,7 +59,8 @@ Some validation is done when reading.
# `SourceDescriptor` object
### Attributes
* `path`: string. Path to the source file. MutatorMath + Varlib.
* `filename`: string. A relative path to the source file, **as it is in the document**. MutatorMath + Varlib.
* `path`: string. Absolute path to the source file, calculated from the document path and the string in the filename attr. MutatorMath + Varlib.
* `name`: string. Unique identifier name of the source, used to identify it if it needs to be referenced from elsewhere in the document. MutatorMath.
* `location`: dict. Axis values for this source. MutatorMath + Varlib
* `copyLib`: bool. Indicates if the contents of the font.lib need to be copied to the instances. MutatorMath.
@ -89,7 +90,8 @@ doc.addSource(s1)
```
# `InstanceDescriptor` object
* `path`: string. Path to the instance file, which may or may not exist. MutatorMath.
* `filename`: string. Relative path to the instance file, **as it is in the document**. The file may or may not exist. MutatorMath.
* `path`: string. Absolute path to the source file, calculated from the document path and the string in the filename attr. The file may or may not exist. MutatorMath.
* `name`: string. Unique identifier name of the instance, used to identify it if it needs to be referenced from elsewhere in the document.
* `location`: dict. Axis values for this source. MutatorMath + Varlib.
* `familyName`: string. Family name of this instance. MutatorMath + Varlib.
@ -456,9 +458,76 @@ myDoc = DesignSpaceDocument(KeyedDocReader, KeyedDocWriter)
<sub name="dollar" byname="dollar.alt"/>
</rule>
</rules>
```
# 6 Notes
## Paths and filenames
A designspace file needs to store many references to UFO files.
* designspace files can be part of versioning systems and appear on different computers. This means it is not possible to store absolute paths.
* So, all paths are relative to the designspace document path.
* Using relative paths allows designspace files and UFO files to be **near** each other, and that they can be **found** without enforcing one particular structure.
* The **filename** attribute in the `SourceDescriptor` and `InstanceDescriptor` classes stores the preferred relative path.
* The **path** attribute in these objects stores the absolute path. It is calculated from the document path and the relative path in the filename attribute when the object is created.
* Only the **filename** attribute is written to file.
Right before we save we need to identify and respond to the following situations:
In each descriptor, we have to do the right thing for the filename attribute. Before writing to file, the `documentObject.updatePaths()` method prepares the paths as follows:
**Case 1**
```
## Notes on this document
descriptor.filename == None
descriptor.path == None
```
**Action**
* write as is, descriptors will not have a filename attr. Useless, but no reason to interfere.
**Case 2**
```
descriptor.filename == "../something"
descriptor.path == None
```
**Action**
* write as is. The filename attr should not be touched.
**Case 3**
```
descriptor.filename == None
descriptor.path == "~/absolute/path/there"
```
**Action**
* calculate the relative path for filename. We're not overwriting some other value for filename, it should be fine.
**Case 4**
```
descriptor.filename == '../somewhere'
descriptor.path == "~/absolute/path/there"
```
**Action**
* There is a conflict between the given filename, and the path. The difference could have happened for any number of reasons. Assuming the values were not in conflict when the object was created, either could have changed. We can't guess.
* Assume the path attribute is more up to date. Calculate a new value for filename based on the path and the document path.
## Recommendation for editors
* If you want to explicitly set the **filename** attribute, leave the path attribute empty.
* If you want to explicitly set the **path** attribute, leave the filename attribute empty. It will be recalculated.
* Use `documentObject.updateFilenameFromPath()` to explicitly set the **filename** attributes for all instance and source descriptors.
# 7 This document
* The package is rather new and changes are to be expected.