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