• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1
2import bjam
3import re
4import types
5
6from itertools import groupby
7
8
9def safe_isinstance(value, types=None, class_names=None):
10    """To prevent circular imports, this extends isinstance()
11    by checking also if `value` has a particular class name (or inherits from a
12    particular class name). This check is safe in that an AttributeError is not
13    raised in case `value` doesn't have a __class__ attribute.
14    """
15    # inspect is being imported here because I seriously doubt
16    # that this function will be used outside of the type
17    # checking below.
18    import inspect
19    result = False
20    if types is not None:
21        result = result or isinstance(value, types)
22    if class_names is not None and not result:
23        # this doesn't work with inheritance, but normally
24        # either the class will already be imported within the module,
25        # or the class doesn't have any subclasses. For example: PropertySet
26        if isinstance(class_names, basestring):
27            class_names = [class_names]
28        # this is the part that makes it "safe".
29        try:
30            base_names = [class_.__name__ for class_ in inspect.getmro(value.__class__)]
31            for name in class_names:
32                if name in base_names:
33                    return True
34        except AttributeError:
35            pass
36    return result
37
38
39def is_iterable_typed(values, type_):
40    return is_iterable(values) and all(isinstance(v, type_) for v in values)
41
42
43def is_iterable(value):
44    """Returns whether value is iterable and not a string."""
45    return not isinstance(value, basestring) and hasattr(value, '__iter__')
46
47
48def is_iterable_or_none(value):
49    return is_iterable(value) or value is None
50
51
52def is_single_value(value):
53    # some functions may specify a bjam signature
54    # that is a string type, but still allow a
55    # PropertySet to be passed in
56    return safe_isinstance(value, (basestring, type(None)), 'PropertySet')
57
58
59if __debug__:
60
61    from textwrap import dedent
62    message = dedent(
63        """The parameter "{}" was passed in a wrong type for the "{}()" function.
64        Actual:
65        \ttype: {}
66        \tvalue: {}
67        Expected:
68        \t{}
69        """
70    )
71
72    bjam_types = {
73        '*': is_iterable_or_none,
74        '+': is_iterable_or_none,
75        '?': is_single_value,
76        '': is_single_value,
77    }
78
79    bjam_to_python = {
80        '*': 'iterable',
81        '+': 'iterable',
82        '?': 'single value',
83        '': 'single value',
84    }
85
86
87    def get_next_var(field):
88        it = iter(field)
89        var = it.next()
90        type_ = None
91        yield_var = False
92        while type_ not in bjam_types:
93            try:
94                # the first value has already
95                # been consumed outside of the loop
96                type_ = it.next()
97            except StopIteration:
98                # if there are no more values, then
99                # var still needs to be returned
100                yield_var = True
101                break
102            if type_ not in bjam_types:
103                # type_ is not a type and is
104                # another variable in the same field.
105                yield var, ''
106                # type_ is the next var
107                var = type_
108            else:
109                # otherwise, type_ is a type for var
110                yield var, type_
111                try:
112                    # the next value should be a var
113                    var = it.next()
114                except StopIteration:
115                    # if not, then we're done with
116                    # this field
117                    break
118        if yield_var:
119            yield var, ''
120
121
122# Decorator the specifies bjam-side prototype for a Python function
123def bjam_signature(s):
124    if __debug__:
125        from inspect import getcallargs
126        def decorator(fn):
127            function_name = fn.__module__ + '.' + fn.__name__
128            def wrapper(*args, **kwargs):
129                callargs = getcallargs(fn, *args, **kwargs)
130                for field in s:
131                    for var, type_ in get_next_var(field):
132                        try:
133                            value = callargs[var]
134                        except KeyError:
135                            raise Exception(
136                                'Bjam Signature specifies a variable named "{}"\n'
137                                'but is not found within the python function signature\n'
138                                'for function {}()'.format(var, function_name)
139                            )
140                        if not bjam_types[type_](value):
141                            raise TypeError(
142                                message.format(var, function_name, type(type_), repr(value),
143                                               bjam_to_python[type_])
144                            )
145                return fn(*args, **kwargs)
146            wrapper.__name__ = fn.__name__
147            wrapper.bjam_signature = s
148            return wrapper
149        return decorator
150    else:
151        def decorator(f):
152            f.bjam_signature = s
153            return f
154
155    return decorator
156
157def metatarget(f):
158
159    f.bjam_signature = (["name"], ["sources", "*"], ["requirements", "*"],
160                        ["default_build", "*"], ["usage_requirements", "*"])
161    return f
162
163class cached(object):
164
165    def __init__(self, function):
166        self.function = function
167        self.cache = {}
168
169    def __call__(self, *args):
170        try:
171            return self.cache[args]
172        except KeyError:
173            v = self.function(*args)
174            self.cache[args] = v
175            return v
176
177    def __get__(self, instance, type):
178        return types.MethodType(self, instance, type)
179
180def unquote(s):
181    if s and s[0] == '"' and s[-1] == '"':
182        return s[1:-1]
183    else:
184        return s
185
186_extract_jamfile_and_rule = re.compile("(Jamfile<.*>)%(.*)")
187
188def qualify_jam_action(action_name, context_module):
189
190    if action_name.startswith("###"):
191        # Callable exported from Python. Don't touch
192        return action_name
193    elif _extract_jamfile_and_rule.match(action_name):
194        # Rule is already in indirect format
195        return action_name
196    else:
197        ix = action_name.find('.')
198        if ix != -1 and action_name[:ix] == context_module:
199            return context_module + '%' + action_name[ix+1:]
200
201        return context_module + '%' + action_name
202
203
204def set_jam_action(name, *args):
205
206    m = _extract_jamfile_and_rule.match(name)
207    if m:
208        args = ("set-update-action-in-module", m.group(1), m.group(2)) + args
209    else:
210        args = ("set-update-action", name) + args
211
212    return bjam.call(*args)
213
214
215def call_jam_function(name, *args):
216
217    m = _extract_jamfile_and_rule.match(name)
218    if m:
219        args = ("call-in-module", m.group(1), m.group(2)) + args
220        return bjam.call(*args)
221    else:
222        return bjam.call(*((name,) + args))
223
224__value_id = 0
225__python_to_jam = {}
226__jam_to_python = {}
227
228def value_to_jam(value, methods=False):
229    """Makes a token to refer to a Python value inside Jam language code.
230
231    The token is merely a string that can be passed around in Jam code and
232    eventually passed back. For example, we might want to pass PropertySet
233    instance to a tag function and it might eventually call back
234    to virtual_target.add_suffix_and_prefix, passing the same instance.
235
236    For values that are classes, we'll also make class methods callable
237    from Jam.
238
239    Note that this is necessary to make a bit more of existing Jamfiles work.
240    This trick should not be used to much, or else the performance benefits of
241    Python port will be eaten.
242    """
243
244    global __value_id
245
246    r = __python_to_jam.get(value, None)
247    if r:
248        return r
249
250    exported_name = '###_' + str(__value_id)
251    __value_id = __value_id + 1
252    __python_to_jam[value] = exported_name
253    __jam_to_python[exported_name] = value
254
255    if methods and type(value) == types.InstanceType:
256        for field_name in dir(value):
257            field = getattr(value, field_name)
258            if callable(field) and not field_name.startswith("__"):
259                bjam.import_rule("", exported_name + "." + field_name, field)
260
261    return exported_name
262
263def record_jam_to_value_mapping(jam_value, python_value):
264    __jam_to_python[jam_value] = python_value
265
266def jam_to_value_maybe(jam_value):
267
268    if type(jam_value) == type(""):
269        return __jam_to_python.get(jam_value, jam_value)
270    else:
271        return jam_value
272
273def stem(filename):
274    i = filename.find('.')
275    if i != -1:
276        return filename[0:i]
277    else:
278        return filename
279
280
281def abbreviate_dashed(s):
282    """Abbreviates each part of string that is delimited by a '-'."""
283    r = []
284    for part in s.split('-'):
285        r.append(abbreviate(part))
286    return '-'.join(r)
287
288
289def abbreviate(s):
290    """Apply a set of standard transformations to string to produce an
291    abbreviation no more than 4 characters long.
292    """
293    if not s:
294        return ''
295    # check the cache
296    if s in abbreviate.abbreviations:
297        return abbreviate.abbreviations[s]
298    # anything less than 4 characters doesn't need
299    # an abbreviation
300    if len(s) < 4:
301        # update cache
302        abbreviate.abbreviations[s] = s
303        return s
304    # save the first character in case it's a vowel
305    s1 = s[0]
306    s2 = s[1:]
307    if s.endswith('ing'):
308        # strip off the 'ing'
309        s2 = s2[:-3]
310    # reduce all doubled characters to one
311    s2 = ''.join(c for c, _ in groupby(s2))
312    # remove all vowels
313    s2 = s2.translate(None, "AEIOUaeiou")
314    # shorten remaining consonants to 4 characters
315    # and add the first char back to the front
316    s2 = s1 + s2[:4]
317    # update cache
318    abbreviate.abbreviations[s] = s2
319    return s2
320# maps key to its abbreviated form
321abbreviate.abbreviations = {}
322