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