File: template.py

package info (click to toggle)
python-enaml 0.19.0-1
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid
  • size: 13,284 kB
  • sloc: python: 31,443; cpp: 4,499; makefile: 140; javascript: 68; lisp: 53; sh: 20
file content (247 lines) | stat: -rw-r--r-- 8,368 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
#------------------------------------------------------------------------------
# Copyright (c) 2013-2025, Nucleic Development Team.
#
# Distributed under the terms of the Modified BSD License.
#
# The full license is in the file LICENSE, distributed with this software.
#------------------------------------------------------------------------------
from types import FunctionType

from bytecode import CompilerFlags
from atom.api import Atom, List, Str, Tuple, Typed

from .compiler_nodes import TemplateNode


class TemplateInstance(Atom):
    """ A class representing a template instantiation.

    Instances of this class are created by instances of Template. They
    should not be created directly by user code.

    """
    #: The template node generated by the specialization function.
    node = Typed(TemplateNode)

    def __call__(self, parent=None, **kwargs):
        """ Instantiate the list of items for the template.

        Parameters
        ----------
        parent : Object, optional
            The parent object for the generated objects.

        **kwargs
            Additional keyword arguments to apply to the returned
            items.

        Returns
        -------
        result : list
            The list of objects generated by the template.

        """
        items = self.node(parent)
        if items and kwargs:
            for item in items:
                for key, value in kwargs.items():
                    setattr(item, key, value)
        return items

    def __getattr__(self, name):
        """ Get the named attribute for the template instance.

        This method will retrieve the value from the template scope, if
        present. Otherwise, it will raise an AttributeError.

        """
        try:
            return self.node.scope[name]
        except KeyError:
            msg = "'%s' object has no attribute '%s'"
            raise AttributeError(msg % (type(self).__name__, name))


class Specialization(Atom):
    """ A class which represents the specialization of a template.

    Instances of this class are created by instances of Template.

    """
    #: The function which builds the TemplateNode.
    func = Typed(FunctionType)

    #: The specialized parameter values for the template.
    paramspec = Tuple()


class Template(Atom):
    """ A class representing a 'template' definition.

    """
    #: The name associated with the template.
    name = Str()

    #: The module name in which the template lives.
    module = Str()

    #: The list of specializations associated with the template. This
    #: list is populated by the compiler.
    specializations = List(Specialization)

    #: The cache of template instantiations.
    cache = Typed(dict, ())

    def __repr__(self):
        """ A nice repr for objects created by the `template` keyword.

        """
        return "<template '%s.%s'>" % (self.module, self.name)

    def make_paramspec(self, items):
        """ Convert the given items into a parameter specification.

        Parameters
        ----------
        items : tuple
            A tuple of parameter objects.

        Returns
        -------
        result : tuple
            A tuple of 2-tuples representing the parameter spec. Each
            2-tuple is of the form (bool, value) where the boolean
            indicates whether the value is a type.

        """
        return tuple((isinstance(item, type), item) for item in items)

    def add_specialization(self, params, func):
        """ Add a specialization to the template.

        Parameters
        ----------
        params : tuple
            A tuple specifying the parameter specializations for the
            positional arguments of the template function. A value of
            None indicates that the parameter can be of any type.

        func : FunctionType
            A function which will return a TemplateNode when invoked
            with user arguments.

        """
        paramspec = self.make_paramspec(params)
        for spec in self.specializations:
            if spec.paramspec == paramspec:
                msg = 'ambiguous template specialization for parameters: %s'
                raise TypeError(msg % (params,))
        spec = Specialization()
        spec.func = func
        spec.paramspec = paramspec
        self.specializations.append(spec)

    def get_specialization(self, args):
        """ Get the specialization for the given arguments.

        Parameters
        ----------
        args : tuple
            A tuple of arguments to match against the current template
            specializations.

        Returns
        -------
        result : Specialization or None
            The best matching specialization for the arguments, or None
            if no match could be found.

        """
        matches = []
        n_args = len(args)
        argspec = None

        for spec in self.specializations:
            # Before scoring for a match, rule out incompatible specs
            # based on the number of arguments. To few arguments is no
            # match, and too many is no match unless the specialization
            # accepts variadic arguments.
            n_params = len(spec.paramspec)
            if n_args < n_params:
                continue
            n_total = n_params + len(spec.func.__defaults__ or ())
            variadic = spec.func.__code__.co_flags & CompilerFlags.VARARGS
            if n_args > n_total and not variadic:
                continue

            # Defer creating the argpec until needed
            if argspec is None:
                argspec = self.make_paramspec(args)

            # Scoring a match is done by ranking the arguments using a
            # closeness measure. If an argument is an exact match to
            # the parameter, it gets a score of 0. If an argument is a
            # subtype of a type parameter, it gets a score equal to the
            # index of the type in the mro of the subtype. If the arg
            # is not an exact match or a subtype, the specialization is
            # not a match. If the parameter has no specialization, the
            # argument gets a score of 1 << 16, which is arbitrary but
            # large enough that it's highly unlikely to be outweighed
            # by any mro type match (1 << 16 subclasses!) and small
            # enough that max_args * (1 << 16) is less that sys.maxint.
            # The default and variadic parameters do not enter into the
            # scoring since the 'add_specialization' method will reject
            # any specialization which is ambiguous. The lowest score
            # wins and a tie will raise an exception.
            score = 0
            items = zip(argspec, spec.paramspec)
            for (a_type, arg), (p_type, param) in items:
                if arg == param:
                    continue
                if param is None:
                    score += 1 << 16
                    continue
                if p_type and a_type and param in arg.__mro__:
                    score += arg.__mro__.index(param)
                    continue
                score = -1
                break
            if score >= 0:
                matches.append((score, spec))

        if matches:
            if len(matches) == 1:
                return matches[0][1]
            matches.sort()
            score_0, match_0 = matches[0]
            score_1, match_1 = matches[1]
            if score_0 == score_1:
                msg = "ambiguous template instantiation for arguments: %s"
                raise TypeError(msg % (args,))
            return match_0

    def __call__(self, *args):
        """ Instantiate the template for the given arguments.

        Parameters
        ----------
        *args
            The arguments to use to instantiate the template.

        Returns
        -------
        result : TemplateInstance
            The instantiated template.

        """
        inst = self.cache.get(args)
        if inst is not None:
            return inst
        spec = self.get_specialization(args)
        if spec is not None:
            inst = TemplateInstance()
            inst.node = spec.func(*args)
            self.cache[args] = inst
            return inst
        msg = 'no matching template specialization for arguments: %s'
        raise TypeError(msg % (args,))