| 1 | # -*- coding: utf-8 -*- |
|---|
| 2 | # |
|---|
| 3 | # Copyright (C) 2010-2015 Roberto Longobardi |
|---|
| 4 | # |
|---|
| 5 | # This file is part of the Test Manager plugin for Trac. |
|---|
| 6 | # |
|---|
| 7 | # This software is licensed as described in the file COPYING, which |
|---|
| 8 | # you should have received as part of this distribution. The terms |
|---|
| 9 | # are also available at: |
|---|
| 10 | # https://trac-hacks.org/wiki/TestManagerForTracPluginLicense |
|---|
| 11 | # |
|---|
| 12 | # Author: Roberto Longobardi <otrebor.dev@gmail.com> |
|---|
| 13 | # |
|---|
| 14 | |
|---|
| 15 | import os |
|---|
| 16 | import re |
|---|
| 17 | import shutil |
|---|
| 18 | import sys |
|---|
| 19 | import traceback |
|---|
| 20 | |
|---|
| 21 | from datetime import datetime |
|---|
| 22 | |
|---|
| 23 | from trac.core import * |
|---|
| 24 | from trac.db import with_transaction |
|---|
| 25 | |
|---|
| 26 | |
|---|
| 27 | def formatExceptionInfo(maxTBlevel=5): |
|---|
| 28 | cla, exc, trbk = sys.exc_info() |
|---|
| 29 | excName = cla.__name__ |
|---|
| 30 | |
|---|
| 31 | try: |
|---|
| 32 | excArgs = exc.__dict__["args"] |
|---|
| 33 | except KeyError: |
|---|
| 34 | excArgs = "<no args>" |
|---|
| 35 | |
|---|
| 36 | excTb = traceback.format_tb(trbk, maxTBlevel) |
|---|
| 37 | |
|---|
| 38 | tracestring = "" |
|---|
| 39 | for step in excTb: |
|---|
| 40 | tracestring += step + "\n" |
|---|
| 41 | |
|---|
| 42 | return "Error name: %s\nArgs: %s\nTraceback:\n%s" % (excName, excArgs, tracestring) |
|---|
| 43 | |
|---|
| 44 | |
|---|
| 45 | checked_utimestamp = False |
|---|
| 46 | has_utimestamp = False |
|---|
| 47 | checked_compatibility = False |
|---|
| 48 | has_read_db = False |
|---|
| 49 | |
|---|
| 50 | def to_any_timestamp(date_obj): |
|---|
| 51 | global checked_utimestamp |
|---|
| 52 | global has_utimestamp |
|---|
| 53 | |
|---|
| 54 | if not checked_utimestamp: |
|---|
| 55 | check_utimestamp() |
|---|
| 56 | |
|---|
| 57 | if has_utimestamp: |
|---|
| 58 | from trac.util.datefmt import to_utimestamp |
|---|
| 59 | return to_utimestamp(date_obj) |
|---|
| 60 | else: |
|---|
| 61 | # Trac 0.11 |
|---|
| 62 | from trac.util.datefmt import to_timestamp |
|---|
| 63 | return to_timestamp(date_obj) |
|---|
| 64 | |
|---|
| 65 | def from_any_timestamp(ts): |
|---|
| 66 | global checked_utimestamp |
|---|
| 67 | global has_utimestamp |
|---|
| 68 | |
|---|
| 69 | if not checked_utimestamp: |
|---|
| 70 | check_utimestamp() |
|---|
| 71 | |
|---|
| 72 | if has_utimestamp: |
|---|
| 73 | from trac.util.datefmt import from_utimestamp |
|---|
| 74 | return from_utimestamp(ts) |
|---|
| 75 | else: |
|---|
| 76 | # Trac 0.11 |
|---|
| 77 | from trac.util.datefmt import utc |
|---|
| 78 | return datetime.fromtimestamp(ts, utc) |
|---|
| 79 | |
|---|
| 80 | def get_db(env, db=None): |
|---|
| 81 | raise TracError('get_db function is deprecated!') |
|---|
| 82 | |
|---|
| 83 | def get_db_for_write(env, db=None): |
|---|
| 84 | raise TracError('get_db_for_write function is deprecated!') |
|---|
| 85 | |
|---|
| 86 | def check_utimestamp(): |
|---|
| 87 | global checked_utimestamp |
|---|
| 88 | global has_utimestamp |
|---|
| 89 | |
|---|
| 90 | try: |
|---|
| 91 | from trac.util.datefmt import to_utimestamp, from_utimestamp |
|---|
| 92 | has_utimestamp = True |
|---|
| 93 | except: |
|---|
| 94 | # Trac 0.11 |
|---|
| 95 | has_utimestamp = False |
|---|
| 96 | |
|---|
| 97 | checked_utimestamp = True |
|---|
| 98 | |
|---|
| 99 | def check_compatibility(env): |
|---|
| 100 | global checked_compatibility |
|---|
| 101 | global has_read_db |
|---|
| 102 | |
|---|
| 103 | try: |
|---|
| 104 | if env.get_read_db(): |
|---|
| 105 | has_read_db = True |
|---|
| 106 | except: |
|---|
| 107 | # Trac 0.11 |
|---|
| 108 | has_read_db = False |
|---|
| 109 | |
|---|
| 110 | checked_compatibility = True |
|---|
| 111 | |
|---|
| 112 | def to_list(params=[]): |
|---|
| 113 | result = [] |
|---|
| 114 | |
|---|
| 115 | for i in params: |
|---|
| 116 | if isinstance(i, list): |
|---|
| 117 | for v in i: |
|---|
| 118 | result.append(v) |
|---|
| 119 | else: |
|---|
| 120 | result.append(i) |
|---|
| 121 | |
|---|
| 122 | return tuple(result) |
|---|
| 123 | |
|---|
| 124 | |
|---|
| 125 | def get_dictionary_from_string(str): |
|---|
| 126 | result = {} |
|---|
| 127 | |
|---|
| 128 | sub = str.partition('{')[2].rpartition('}')[0] |
|---|
| 129 | tokens = sub.split(",") |
|---|
| 130 | |
|---|
| 131 | for tok in tokens: |
|---|
| 132 | name = remove_quotes(tok.partition(':')[0]) |
|---|
| 133 | value = remove_quotes(tok.partition(':')[2]) |
|---|
| 134 | |
|---|
| 135 | result[name] = value |
|---|
| 136 | |
|---|
| 137 | return result |
|---|
| 138 | |
|---|
| 139 | |
|---|
| 140 | def get_string_from_dictionary(dictionary, values=None): |
|---|
| 141 | if values is None: |
|---|
| 142 | values = dictionary |
|---|
| 143 | |
|---|
| 144 | result = '{' |
|---|
| 145 | for i, k in enumerate(dictionary): |
|---|
| 146 | # TODO: send a patch for the following line |
|---|
| 147 | # result += "'"+k+"':'"+values[k]+"'" # old code not working for int values |
|---|
| 148 | result += "'%s':'%s'" % (k, values[k]) |
|---|
| 149 | if i < len(dictionary)-1: |
|---|
| 150 | result += "," |
|---|
| 151 | |
|---|
| 152 | result += '}' |
|---|
| 153 | |
|---|
| 154 | return result |
|---|
| 155 | |
|---|
| 156 | |
|---|
| 157 | def remove_quotes(str, quote='\''): |
|---|
| 158 | return str.partition(quote)[2].rpartition(quote)[0] |
|---|
| 159 | |
|---|
| 160 | |
|---|
| 161 | def compatible_domain_functions(domain, function_name_list): |
|---|
| 162 | return lambda x: x, lambda x: x, lambda x: x, lambda x: x |
|---|
| 163 | |
|---|
| 164 | def get_timestamp_db_type(): |
|---|
| 165 | global checked_utimestamp |
|---|
| 166 | global has_utimestamp |
|---|
| 167 | |
|---|
| 168 | if not checked_utimestamp: |
|---|
| 169 | check_utimestamp() |
|---|
| 170 | |
|---|
| 171 | if has_utimestamp: |
|---|
| 172 | return 'int64' |
|---|
| 173 | else: |
|---|
| 174 | # Trac 0.11 |
|---|
| 175 | return 'int' |
|---|
| 176 | |
|---|
| 177 | def upload_file_to_subdir(env, req, subdir, param_name, target_filename): |
|---|
| 178 | upload = param_name |
|---|
| 179 | |
|---|
| 180 | if isinstance(upload, unicode) or not upload.filename: |
|---|
| 181 | raise TracError('You must provide a file.') |
|---|
| 182 | |
|---|
| 183 | txt_filename = upload.filename.replace('\\', '/').replace(':', '/') |
|---|
| 184 | txt_filename = os.path.basename(txt_filename) |
|---|
| 185 | if not txt_filename: |
|---|
| 186 | raise TracError('You must provide a file.') |
|---|
| 187 | |
|---|
| 188 | target_dir = os.path.join(env.path, 'upload', subdir) |
|---|
| 189 | |
|---|
| 190 | if not os.access(target_dir, os.F_OK): |
|---|
| 191 | os.makedirs(target_dir) |
|---|
| 192 | |
|---|
| 193 | target_path = os.path.join(target_dir, target_filename) |
|---|
| 194 | |
|---|
| 195 | try: |
|---|
| 196 | target_file = open(target_path, 'w') |
|---|
| 197 | shutil.copyfileobj(upload.file, target_file) |
|---|
| 198 | finally: |
|---|
| 199 | target_file.close() |
|---|
| 200 | |
|---|
| 201 | |
|---|
| 202 | def db_insert_or_ignore(env, tablename, propname, value, db=None): |
|---|
| 203 | if db_get_config_property(env, tablename, propname, db) is None: |
|---|
| 204 | db_set_config_property(env, tablename, propname, value, db) |
|---|
| 205 | |
|---|
| 206 | def db_get_config_property(env, tablename, propname, db=None): |
|---|
| 207 | if not db: |
|---|
| 208 | db = env.get_read_db() |
|---|
| 209 | |
|---|
| 210 | cursor = db.cursor() |
|---|
| 211 | |
|---|
| 212 | sql = "SELECT value FROM %s WHERE propname=%%s" % tablename |
|---|
| 213 | row = None |
|---|
| 214 | |
|---|
| 215 | try: |
|---|
| 216 | cursor.execute(sql, (propname,)) |
|---|
| 217 | row = cursor.fetchone() |
|---|
| 218 | except: |
|---|
| 219 | pass |
|---|
| 220 | |
|---|
| 221 | if not row or len(row) == 0: |
|---|
| 222 | return None |
|---|
| 223 | |
|---|
| 224 | return row[0] |
|---|
| 225 | |
|---|
| 226 | def db_set_config_property(env, tablename, propname, value, db=None): |
|---|
| 227 | @env.with_transaction(db) |
|---|
| 228 | def do_db_set_config_property(db): |
|---|
| 229 | cursor = db.cursor() |
|---|
| 230 | |
|---|
| 231 | sql = "SELECT COUNT(*) FROM %s WHERE propname = %%s" % tablename |
|---|
| 232 | cursor.execute(sql, (propname,)) |
|---|
| 233 | |
|---|
| 234 | row = cursor.fetchone() |
|---|
| 235 | if row is not None and int(row[0]) > 0: |
|---|
| 236 | cursor.execute(""" |
|---|
| 237 | UPDATE %s |
|---|
| 238 | SET value = %%s |
|---|
| 239 | WHERE propname = %%s |
|---|
| 240 | """ % tablename, (str(value), propname)) |
|---|
| 241 | else: |
|---|
| 242 | cursor.execute(""" |
|---|
| 243 | INSERT INTO %s (propname,value) |
|---|
| 244 | VALUES (%%s,%%s) |
|---|
| 245 | """ % tablename, (propname, str(value))) |
|---|
| 246 | |
|---|
| 247 | return True |
|---|
| 248 | |
|---|
| 249 | def list_available_tables(dburi, cursor): |
|---|
| 250 | if dburi.startswith('sqlite:'): |
|---|
| 251 | query = """ |
|---|
| 252 | SELECT name FROM sqlite_master |
|---|
| 253 | WHERE type='table' AND NOT name='sqlite_sequence' |
|---|
| 254 | """ |
|---|
| 255 | elif dburi.startswith('postgres:'): |
|---|
| 256 | query = """ |
|---|
| 257 | SELECT tablename FROM pg_tables |
|---|
| 258 | WHERE schemaname = ANY (current_schemas(false)) |
|---|
| 259 | """ |
|---|
| 260 | elif dburi.startswith('mysql:'): |
|---|
| 261 | query = "SHOW TABLES" |
|---|
| 262 | else: |
|---|
| 263 | raise TracError('Unsupported %s database' % dburi.split(':')[0]) |
|---|
| 264 | cursor.execute(query) |
|---|
| 265 | |
|---|
| 266 | return sorted([row[0] for row in cursor]) |
|---|
| 267 | |
|---|
| 268 | def fix_base_location(req): |
|---|
| 269 | return req.href('/').rstrip('/') |
|---|
| 270 | |
|---|
| 271 | |
|---|
| 272 | ## {{{ http://code.activestate.com/recipes/550804/ (r2) |
|---|
| 273 | # The list of symbols that are included by default in the generated |
|---|
| 274 | # function's environment |
|---|
| 275 | SAFE_SYMBOLS = ["list", "dict", "tuple", "set", "long", "float", "object", |
|---|
| 276 | "bool", "callable", "True", "False", "dir", |
|---|
| 277 | "frozenset", "getattr", "hasattr", "abs", "cmp", "complex", |
|---|
| 278 | "divmod", "id", "pow", "round", "slice", "vars", |
|---|
| 279 | "hash", "hex", "int", "isinstance", "issubclass", "len", |
|---|
| 280 | "map", "filter", "max", "min", "oct", "chr", "ord", "range", |
|---|
| 281 | "reduce", "repr", "str", "type", "zip", "xrange", "None", |
|---|
| 282 | "Exception", "KeyboardInterrupt"] |
|---|
| 283 | # Also add the standard exceptions |
|---|
| 284 | __bi = __builtins__ |
|---|
| 285 | if type(__bi) is not dict: |
|---|
| 286 | __bi = __bi.__dict__ |
|---|
| 287 | for k in __bi: |
|---|
| 288 | if k.endswith("Error") or k.endswith("Warning"): |
|---|
| 289 | SAFE_SYMBOLS.append(k) |
|---|
| 290 | del __bi |
|---|
| 291 | |
|---|
| 292 | def createFunctionFromString(sourceCode, args="", additional_symbols=dict()): |
|---|
| 293 | """ |
|---|
| 294 | Create a python function from the given source code |
|---|
| 295 | |
|---|
| 296 | \param sourceCode A python string containing the core of the |
|---|
| 297 | function. Might include the return statement (or not), definition of |
|---|
| 298 | local functions, classes, etc. Indentation matters ! |
|---|
| 299 | |
|---|
| 300 | \param args The string representing the arguments to put in the function's |
|---|
| 301 | prototype, such as "a, b", or "a=12, b", |
|---|
| 302 | or "a=12, b=dict(akey=42, another=5)" |
|---|
| 303 | |
|---|
| 304 | \param additional_symbols A dictionary variable name => |
|---|
| 305 | variable/funcion/object to include in the generated function's |
|---|
| 306 | closure |
|---|
| 307 | |
|---|
| 308 | The sourceCode will be executed in a restricted environment, |
|---|
| 309 | containing only the python builtins that are harmless (such as map, |
|---|
| 310 | hasattr, etc.). To allow the function to access other modules or |
|---|
| 311 | functions or objects, use the additional_symbols parameter. For |
|---|
| 312 | example, to allow the source code to access the re and sys modules, |
|---|
| 313 | as well as a global function F named afunction in the sourceCode and |
|---|
| 314 | an object OoO named ooo in the sourceCode, specify: |
|---|
| 315 | additional_symbols = dict(re=re, sys=sys, afunction=F, ooo=OoO) |
|---|
| 316 | |
|---|
| 317 | \return A python function implementing the source code. It can be |
|---|
| 318 | recursive: the (internal) name of the function being defined is: |
|---|
| 319 | __TheFunction__. Its docstring is the initial sourceCode string. |
|---|
| 320 | |
|---|
| 321 | Tests show that the resulting function does not have any calling |
|---|
| 322 | time overhead (-3% to +3%, probably due to system preemption aleas) |
|---|
| 323 | compared to normal python function calls. |
|---|
| 324 | """ |
|---|
| 325 | # Include the sourcecode as the code of a function __TheFunction__: |
|---|
| 326 | s = "def __TheFunction__(%s):\n" % args |
|---|
| 327 | s += "\t" + "\n\t".join(sourceCode.split('\n')) + "\n" |
|---|
| 328 | |
|---|
| 329 | # Byte-compilation (optional) |
|---|
| 330 | byteCode = compile(s, "<string>", 'exec') |
|---|
| 331 | |
|---|
| 332 | # Setup the local and global dictionaries of the execution |
|---|
| 333 | # environment for __TheFunction__ |
|---|
| 334 | bis = dict() # builtins |
|---|
| 335 | globs = dict() |
|---|
| 336 | locs = dict() |
|---|
| 337 | |
|---|
| 338 | # Setup a standard-compatible python environment |
|---|
| 339 | bis["locals"] = lambda: locs |
|---|
| 340 | bis["globals"] = lambda: globs |
|---|
| 341 | globs["__builtins__"] = bis |
|---|
| 342 | globs["__name__"] = "SUBENV" |
|---|
| 343 | globs["__doc__"] = sourceCode |
|---|
| 344 | |
|---|
| 345 | # Determine how the __builtins__ dictionary should be accessed |
|---|
| 346 | if type(__builtins__) is dict: |
|---|
| 347 | bi_dict = __builtins__ |
|---|
| 348 | else: |
|---|
| 349 | bi_dict = __builtins__.__dict__ |
|---|
| 350 | |
|---|
| 351 | # Include the safe symbols |
|---|
| 352 | for k in SAFE_SYMBOLS: |
|---|
| 353 | # try from current locals |
|---|
| 354 | try: |
|---|
| 355 | locs[k] = locals()[k] |
|---|
| 356 | continue |
|---|
| 357 | except KeyError: |
|---|
| 358 | pass |
|---|
| 359 | # Try from globals |
|---|
| 360 | try: |
|---|
| 361 | globs[k] = globals()[k] |
|---|
| 362 | continue |
|---|
| 363 | except KeyError: |
|---|
| 364 | pass |
|---|
| 365 | # Try from builtins |
|---|
| 366 | try: |
|---|
| 367 | bis[k] = bi_dict[k] |
|---|
| 368 | except KeyError: |
|---|
| 369 | # Symbol not available anywhere: silently ignored |
|---|
| 370 | pass |
|---|
| 371 | |
|---|
| 372 | # Include the symbols added by the caller, in the globals dictionary |
|---|
| 373 | globs.update(additional_symbols) |
|---|
| 374 | |
|---|
| 375 | # Finally execute the def __TheFunction__ statement: |
|---|
| 376 | eval(byteCode, globs, locs) |
|---|
| 377 | # As a result, the function is defined as the item __TheFunction__ |
|---|
| 378 | # in the locals dictionary |
|---|
| 379 | fct = locs["__TheFunction__"] |
|---|
| 380 | # Attach the function to the globals so that it can be recursive |
|---|
| 381 | del locs["__TheFunction__"] |
|---|
| 382 | globs["__TheFunction__"] = fct |
|---|
| 383 | # Attach the actual source code to the docstring |
|---|
| 384 | fct.__doc__ = sourceCode |
|---|
| 385 | return fct |
|---|
| 386 | ## end of http://code.activestate.com/recipes/550804/ }}} |
|---|
| 387 | |
|---|
| 388 | # Example: |
|---|
| 389 | # |
|---|
| 390 | # import sys, re |
|---|
| 391 | # |
|---|
| 392 | # f = createFunction("print a\nprint b", "a=3, b=4", |
|---|
| 393 | # additional_symbols = dict(re=re, sys=sys, |
|---|
| 394 | # afunction=F, ooo=OoO)) |
|---|
| 395 | # f() |
|---|
| 396 | # f(12) |
|---|
| 397 | # f(b=7, a=8) |
|---|
| 398 | # f(9, 10) |
|---|