<commit_before>"""
A library for importing .ufo files and their descendants.
Refer to http://unifiedfontobject.com for the UFO specification.

The UFOReader and UFOWriter classes support versions 1, 2 and 3
of the specification.

Sets that list the font info attribute names for the fontinfo.plist
formats are available for external use. These are:
	fontInfoAttributesVersion1
	fontInfoAttributesVersion2
	fontInfoAttributesVersion3

A set listing the fontinfo.plist attributes that were deprecated
in version 2 is available for external use:
	deprecatedFontInfoAttributesVersion2

Functions that do basic validation on values for fontinfo.plist
are available for external use. These are
	validateFontInfoVersion2ValueForAttribute
	validateFontInfoVersion3ValueForAttribute

Value conversion functions are availble for converting
fontinfo.plist values between the possible format versions.
	convertFontInfoValueForAttributeFromVersion1ToVersion2
	convertFontInfoValueForAttributeFromVersion2ToVersion1
	convertFontInfoValueForAttributeFromVersion2ToVersion3
	convertFontInfoValueForAttributeFromVersion3ToVersion2
"""


import os
import shutil
from cStringIO import StringIO
import codecs
from copy import deepcopy
from plistlib import readPlist, writePlist
from glifLib import GlyphSet, READ_MODE, WRITE_MODE
from validators import *
from filenames import userNameToFileName
from converters import convertUFO1OrUFO2KerningToUFO3Kerning

try:
	set
except NameError:
	from sets import Set as set

__all__ = [
	"makeUFOPath"
	"UFOLibError",
	"UFOReader",
	"UFOWriter",
	"fontInfoAttributesVersion1",
	"fontInfoAttributesVersion2",
	"fontInfoAttributesVersion3",
	"deprecatedFontInfoAttributesVersion2",
	"validateFontInfoVersion2ValueForAttribute",
	"validateFontInfoVersion3ValueForAttribute",
	"convertFontInfoValueForAttributeFromVersion1ToVersion2",
	"convertFontInfoValueForAttributeFromVersion2ToVersion1"
	# deprecated
	"convertUFOFormatVersion1ToFormatVersion2"
]


class UFOLibError(Exception): pass


# ----------
# File Names
# ----------

DEFAULT_GLYPHS_DIRNAME = "glyphs"
DATA_DIRNAME = "data"
IMAGES_DIRNAME = "images"
METAINFO_FILENAME = "metainfo.plist"
FONTINFO_FILENAME = "fontinfo.plist"
LIB_FILENAME = "lib.plist"	
GROUPS_FILENAME = "groups.plist"
KERNING_FILENAME = "kerning.plist"
FEATURES_FILENAME = "features.fea"
LAYERCONTENTS_FILENAME = "layercontents.plist"
LAYERINFO_FILENAME = "layerinfo.plist"

DEFAULT_LAYER_NAME = "public.default"

supportedUFOFormatVersions = [1, 2, 3]


# ----------
# UFO Reader
# ----------

class UFOReader(object):

	"""Read the various components of the .ufo."""

	def __init__(self, path):
		if not os.path.exists(path):
			raise UFOLibError("The specified UFO doesn't exist.")
		self._path = path
		self.readMetaInfo()
		self._upConvertedKerningData = None

	# properties

	def _get_formatVersion(self):
		return self._formatVersion

	formatVersion = property(_get_formatVersion, doc="The format version of the UFO. This is determined by reading metainfo.plist during __init__.")

	# up conversion

	def _upConvertKerning(self):
		"""
		Up convert kerning and groups in UFO 1 and 2.
		The data will be held internally until each bit of data
		has been retrieved. The conversion of both must be done
		at once, so the raw data is cached and an error is raised
		if one bit of data becomes obsolete before it is called.
		"""
		if self._upConvertedKerningData:
			testKerning = self._readKerning()
			if testKerning != self._upConvertedKerningData["originalKerning"]:
				raise UFOLibError("The data in kerning.plist has been modified since it was converted to UFO 3 format.")
			testGroups = self._readGroups()
			if testGroups != self._upConvertedKerningData["originalGroups"]:
				raise UFOLibError("The data in groups.plist has been modified since it was converted to UFO 3 format.")
		else:
			groups = self._readGroups()
			invalidFormatMessage = "groups.plist is not properly formatted."
			if not isinstance(groups, dict):
				raise UFOLibError(invalidFormatMessage)
			for groupName, glyphList in groups.items():
				if not isinstance(groupName, basestring):
					raise UFOLibError(invalidFormatMessage)
				elif not isinstance(glyphList, list):
					raise UFOLibError(invalidFormatMessage)
				for glyphName in glyphList:
					if not isinstance(glyphName, basestring):
						raise UFOLibError(invalidFormatMessage)
			self._upConvertedKerningData = dict(
				kerning={},
				originalKerning=self._readKerning(),
				groups={},
				originalGroups=groups
			)
			# convert kerning and groups
			kerning, groups, conversionMaps = convertUFO1OrUFO2KerningToUFO3Kerning(
				self._upConvertedKerningData["originalKerning"],
				deepcopy(self._upConvertedKerningData["originalGroups"])
			)
			# store
			self._upConvertedKerningData["kerning"] = kerning
			self._upConvertedKerningData["groups"] = groups
			self._upConvertedKerningData["groupRenameMaps"] = conversionMaps

	# support methods

	_checkForFile = staticmethod(os.path.exists)

	def _getPlist(self, fileName, default=None):
		"""
		Read a property list relative to the
		path argument of UFOReader. If the file
		is missing and default is None a
		UFOLibError will be raised otherwise
		default is returned. The errors that
		could be raised during the reading of
		a plist are unpredictable and/or too
		large to list, so, a blind try: except:
		is done. If an exception occurs, a
		UFOLibError will be raised.
		"""
		path = os.path.join(self._path, fileName)
		if not os.path.exists(path):
			if default is not None:
				return default
			else:
				raise UFOLibError("%s is missing in %s. This file is required" % (fileName, self._path))
		try:
			return readPlist(path)
		except:
			raise UFOLibError("The file %s could not be read." % fileName)

	def readBytesFromPath(self, path, encoding=None):
		"""
		Returns the bytes in the file at the given path.<commit_msg>prepare sharing of UFOReader._getPlist implementation<commit_after>"""
A library for importing .ufo files and their descendants.
Refer to http://unifiedfontobject.com for the UFO specification.

The UFOReader and UFOWriter classes support versions 1, 2 and 3
of the specification.

Sets that list the font info attribute names for the fontinfo.plist
formats are available for external use. These are:
	fontInfoAttributesVersion1
	fontInfoAttributesVersion2
	fontInfoAttributesVersion3

A set listing the fontinfo.plist attributes that were deprecated
in version 2 is available for external use:
	deprecatedFontInfoAttributesVersion2

Functions that do basic validation on values for fontinfo.plist
are available for external use. These are
	validateFontInfoVersion2ValueForAttribute
	validateFontInfoVersion3ValueForAttribute

Value conversion functions are availble for converting
fontinfo.plist values between the possible format versions.
	convertFontInfoValueForAttributeFromVersion1ToVersion2
	convertFontInfoValueForAttributeFromVersion2ToVersion1
	convertFontInfoValueForAttributeFromVersion2ToVersion3
	convertFontInfoValueForAttributeFromVersion3ToVersion2
"""


import os
import shutil
from cStringIO import StringIO
import codecs
from copy import deepcopy
from plistlib import readPlist, writePlist
from glifLib import GlyphSet, READ_MODE, WRITE_MODE
from validators import *
from filenames import userNameToFileName
from converters import convertUFO1OrUFO2KerningToUFO3Kerning

try:
	set
except NameError:
	from sets import Set as set

__all__ = [
	"makeUFOPath"
	"UFOLibError",
	"UFOReader",
	"UFOWriter",
	"fontInfoAttributesVersion1",
	"fontInfoAttributesVersion2",
	"fontInfoAttributesVersion3",
	"deprecatedFontInfoAttributesVersion2",
	"validateFontInfoVersion2ValueForAttribute",
	"validateFontInfoVersion3ValueForAttribute",
	"convertFontInfoValueForAttributeFromVersion1ToVersion2",
	"convertFontInfoValueForAttributeFromVersion2ToVersion1"
	# deprecated
	"convertUFOFormatVersion1ToFormatVersion2"
]


class UFOLibError(Exception): pass


# ----------
# File Names
# ----------

DEFAULT_GLYPHS_DIRNAME = "glyphs"
DATA_DIRNAME = "data"
IMAGES_DIRNAME = "images"
METAINFO_FILENAME = "metainfo.plist"
FONTINFO_FILENAME = "fontinfo.plist"
LIB_FILENAME = "lib.plist"	
GROUPS_FILENAME = "groups.plist"
KERNING_FILENAME = "kerning.plist"
FEATURES_FILENAME = "features.fea"
LAYERCONTENTS_FILENAME = "layercontents.plist"
LAYERINFO_FILENAME = "layerinfo.plist"

DEFAULT_LAYER_NAME = "public.default"

supportedUFOFormatVersions = [1, 2, 3]


# --------------
# Shared Methods
# --------------

def _getPlist(self, fileName, default=None):
	"""
	Read a property list relative to the
	path argument of UFOReader. If the file
	is missing and default is None a
	UFOLibError will be raised otherwise
	default is returned. The errors that
	could be raised during the reading of
	a plist are unpredictable and/or too
	large to list, so, a blind try: except:
	is done. If an exception occurs, a
	UFOLibError will be raised.
	"""
	path = os.path.join(self._path, fileName)
	if not os.path.exists(path):
		if default is not None:
			return default
		else:
			raise UFOLibError("%s is missing in %s. This file is required" % (fileName, self._path))
	try:
		return readPlist(path)
	except:
		raise UFOLibError("The file %s could not be read." % fileName)

# ----------
# UFO Reader
# ----------

class UFOReader(object):

	"""Read the various components of the .ufo."""

	def __init__(self, path):
		if not os.path.exists(path):
			raise UFOLibError("The specified UFO doesn't exist.")
		self._path = path
		self.readMetaInfo()
		self._upConvertedKerningData = None

	# properties

	def _get_formatVersion(self):
		return self._formatVersion

	formatVersion = property(_get_formatVersion, doc="The format version of the UFO. This is determined by reading metainfo.plist during __init__.")

	# up conversion

	def _upConvertKerning(self):
		"""
		Up convert kerning and groups in UFO 1 and 2.
		The data will be held internally until each bit of data
		has been retrieved. The conversion of both must be done
		at once, so the raw data is cached and an error is raised
		if one bit of data becomes obsolete before it is called.
		"""
		if self._upConvertedKerningData:
			testKerning = self._readKerning()
			if testKerning != self._upConvertedKerningData["originalKerning"]:
				raise UFOLibError("The data in kerning.plist has been modified since it was converted to UFO 3 format.")
			testGroups = self._readGroups()
			if testGroups != self._upConvertedKerningData["originalGroups"]:
				raise UFOLibError("The data in groups.plist has been modified since it was converted to UFO 3 format.")
		else:
			groups = self._readGroups()
			invalidFormatMessage = "groups.plist is not properly formatted."
			if not isinstance(groups, dict):
				raise UFOLibError(invalidFormatMessage)
			for groupName, glyphList in groups.items():
				if not isinstance(groupName, basestring):
					raise UFOLibError(invalidFormatMessage)
				elif not isinstance(glyphList, list):
					raise UFOLibError(invalidFormatMessage)
				for glyphName in glyphList:
					if not isinstance(glyphName, basestring):
						raise UFOLibError(invalidFormatMessage)
			self._upConvertedKerningData = dict(
				kerning={},
				originalKerning=self._readKerning(),
				groups={},
				originalGroups=groups
			)
			# convert kerning and groups
			kerning, groups, conversionMaps = convertUFO1OrUFO2KerningToUFO3Kerning(
				self._upConvertedKerningData["originalKerning"],
				deepcopy(self._upConvertedKerningData["originalGroups"])
			)
			# store
			self._upConvertedKerningData["kerning"] = kerning
			self._upConvertedKerningData["groups"] = groups
			self._upConvertedKerningData["groupRenameMaps"] = conversionMaps

	# support methods

	_checkForFile = staticmethod(os.path.exists)

	_getPlist = _getPlist

	def readBytesFromPath(self, path, encoding=None):
		"""
		Returns the bytes in the file at the given path.
{'fraction_non_alphanumeric': 0.07737644186242316, 'fraction_numerical': 0.0072408857455586425, 'mean_word_length': 4.9271457085828345, 'pattern_counts': {'":': 0, '<': 3, '<?xml version=': 0, '>': 3, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '3061773', 'n_tokens_mistral': 3931, 'n_tokens_neox': 3496, 'n_words': 1093}