1from __future__ import ( 2 print_function, 3 absolute_import, 4 division, 5 unicode_literals, 6) 7from .baseFeatureWriter import BaseFeatureWriter 8from .kernFeatureWriter import KernFeatureWriter 9from .markFeatureWriter import MarkFeatureWriter 10 11import importlib 12import re 13from inspect import isclass 14 15try: 16 from inspect import getfullargspec as getargspec # PY3 17except ImportError: 18 from inspect import getargspec # PY2 19import logging 20 21 22__all__ = [ 23 "BaseFeatureWriter", 24 "KernFeatureWriter", 25 "MarkFeatureWriter", 26 "loadFeatureWriters", 27] 28 29logger = logging.getLogger(__name__) 30 31FEATURE_WRITERS_KEY = "com.github.googlei18n.ufo2ft.featureWriters" 32 33 34def isValidFeatureWriter(klass): 35 """Return True if 'klass' is a valid feature writer class. 36 A valid feature writer class is a class (of type 'type'), that has 37 two required attributes: 38 1) 'tableTag' (str), which can be "GSUB", "GPOS", or other similar tags. 39 2) 'write' (bound method), with the signature matching the same method 40 from the BaseFeatureWriter class: 41 42 def write(self, font, feaFile, compiler=None) 43 """ 44 if not isclass(klass): 45 logger.error("%r is not a class", klass) 46 return False 47 if not hasattr(klass, "tableTag"): 48 logger.error("%r does not have required 'tableTag' attribute", klass) 49 return False 50 if not hasattr(klass, "write"): 51 logger.error("%r does not have a required 'write' method", klass) 52 return False 53 if ( 54 getargspec(klass.write).args 55 != getargspec(BaseFeatureWriter.write).args 56 ): 57 logger.error("%r 'write' method has incorrect signature", klass) 58 return False 59 return True 60 61 62def loadFeatureWriters(ufo, ignoreErrors=True): 63 """Check UFO lib for key "com.github.googlei18n.ufo2ft.featureWriters", 64 containing a list of dicts, each having the following key/value pairs: 65 For example: 66 67 { 68 "module": "myTools.featureWriters", # default: ufo2ft.featureWriters 69 "class": "MyKernFeatureWriter", # required 70 "options": {"doThis": False, "doThat": True}, 71 } 72 73 Import each feature writer class from the specified module (default is 74 the built-in ufo2ft.featureWriters), and instantiate it with the given 75 'options' dict. 76 77 Return the list of feature writer objects. 78 If the 'featureWriters' key is missing from the UFO lib, return None. 79 80 If an exception occurs and 'ignoreErrors' is True, the exception message 81 is logged and the invalid writer is skipped, otrherwise it's propagated. 82 """ 83 if FEATURE_WRITERS_KEY not in ufo.lib: 84 return None 85 writers = [] 86 for wdict in ufo.lib[FEATURE_WRITERS_KEY]: 87 try: 88 moduleName = wdict.get("module", __name__) 89 className = wdict["class"] 90 options = wdict.get("options", {}) 91 if not isinstance(options, dict): 92 raise TypeError(type(options)) 93 module = importlib.import_module(moduleName) 94 klass = getattr(module, className) 95 if not isValidFeatureWriter(klass): 96 raise TypeError(klass) 97 writer = klass(**options) 98 except Exception: 99 if ignoreErrors: 100 logger.exception("failed to load feature writer: %r", wdict) 101 continue 102 raise 103 writers.append(writer) 104 return writers 105 106 107# NOTE about the security risk involved in using eval: the function below is 108# meant to be used to parse string coming from the command-line, which is 109# inherently "trusted"; if that weren't the case, a potential attacker 110# could do worse things than segfaulting the Python interpreter... 111 112 113def _kwargsEval(s): 114 return eval( 115 "dict(%s)" % s, 116 {"__builtins__": {"True": True, "False": False, "dict": dict}}, 117 ) 118 119 120_featureWriterSpecRE = re.compile( 121 r"(?:([\w\.]+)::)?" # MODULE_NAME + '::' 122 r"(\w+)" # CLASS_NAME [required] 123 r"(?:\((.*)\))?" # (KWARGS) 124) 125 126 127def loadFeatureWriterFromString(spec): 128 """ Take a string specifying a feature writer class to load (either a 129 built-in writer or one defined in an external, user-defined module), 130 initialize it with given options and return the writer object. 131 132 The string must conform to the following notation: 133 - an optional python module, followed by '::' 134 - a required class name; the class must have a method call 'write' 135 with the same signature as the BaseFeatureWriter. 136 - an optional list of keyword-only arguments enclosed by parentheses 137 138 Raises ValueError if the string doesn't conform to this specification; 139 TypeError if imported name is not a feature writer class; and 140 ImportError if the user-defined module cannot be imported. 141 142 Examples: 143 144 >>> loadFeatureWriterFromString("KernFeatureWriter") 145 <ufo2ft.featureWriters.kernFeatureWriter.KernFeatureWriter object at ...> 146 >>> w = loadFeatureWriterFromString("KernFeatureWriter(ignoreMarks=False)") 147 >>> w.options.ignoreMarks 148 False 149 >>> w = loadFeatureWriterFromString("MarkFeatureWriter(features=['mkmk'])") 150 >>> w.features == frozenset(['mkmk']) 151 True 152 >>> loadFeatureWriterFromString("ufo2ft.featureWriters::KernFeatureWriter") 153 <ufo2ft.featureWriters.kernFeatureWriter.KernFeatureWriter object at ...> 154 """ 155 spec = spec.strip() 156 m = _featureWriterSpecRE.match(spec) 157 if not m or (m.end() - m.start()) != len(spec): 158 raise ValueError(spec) 159 moduleName = m.group(1) or "ufo2ft.featureWriters" 160 className = m.group(2) 161 kwargs = m.group(3) 162 163 module = importlib.import_module(moduleName) 164 klass = getattr(module, className) 165 if not isValidFeatureWriter(klass): 166 raise TypeError(klass) 167 try: 168 options = _kwargsEval(kwargs) if kwargs else {} 169 except SyntaxError: 170 raise ValueError("options have incorrect format: %r" % kwargs) 171 return klass(**options) 172